From 2b7560b1e71a2d12904ae604f123ab932beb7abe Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 27 Oct 2022 14:21:14 +0100 Subject: [PATCH 001/197] Add Labs-Z label for rich text editor and migrate to new label naming --- .github/workflows/triage-labelled.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index cdb354be833..a542dacfb95 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -17,7 +17,8 @@ jobs: contains(github.event.issue.labels.*.name, 'Z-IA') || contains(github.event.issue.labels.*.name, 'A-Themes-Custom') || contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || - contains(github.event.issue.labels.*.name, 'A-Tags') + contains(github.event.issue.labels.*.name, 'A-Tags') || + contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') steps: - uses: actions/github-script@v5 with: @@ -311,7 +312,7 @@ jobs: name: Add labelled issues to PS features team 3 runs-on: ubuntu-latest if: > - contains(github.event.issue.labels.*.name, 'A-Composer-WYSIWYG') + contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') steps: - uses: octokit/graphql-action@v2.x id: add_to_project From 15583a14aad1fddcf44076a9e354a05dcf8c685c Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 27 Oct 2022 14:30:36 +0100 Subject: [PATCH 002/197] changelog --- changelog.d/7477.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7477.misc diff --git a/changelog.d/7477.misc b/changelog.d/7477.misc new file mode 100644 index 00000000000..2ea83ce81d5 --- /dev/null +++ b/changelog.d/7477.misc @@ -0,0 +1 @@ +Add Z-Labs label for rich text editor and migrate to new label naming. \ No newline at end of file From e4caf7be8127961488b1d67e9461b0c4d39433c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Nov 2022 23:03:53 +0000 Subject: [PATCH 003/197] Bump barista from 4.2.0 to 4.3.0 Bumps [barista](https://github.com/AdevintaSpain/Barista) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/AdevintaSpain/Barista/releases) - [Commits](https://github.com/AdevintaSpain/Barista/compare/4.2.0...4.3.0) --- updated-dependencies: - dependency-name: com.adevinta.android:barista dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- vector-app/build.gradle | 2 +- vector/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vector-app/build.gradle b/vector-app/build.gradle index bff01935094..50057002575 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -406,7 +406,7 @@ dependencies { // Plant Timber tree for test androidTestImplementation libs.tests.timberJunitRule // "The one who serves a great Espresso" - androidTestImplementation('com.adevinta.android:barista:4.2.0') { + androidTestImplementation('com.adevinta.android:barista:4.3.0') { exclude group: 'org.jetbrains.kotlin' } androidTestImplementation libs.mockk.mockkAndroid diff --git a/vector/build.gradle b/vector/build.gradle index 890236422e8..afdd589de6a 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -325,7 +325,7 @@ dependencies { // Plant Timber tree for test androidTestImplementation libs.tests.timberJunitRule // "The one who serves a great Espresso" - androidTestImplementation('com.adevinta.android:barista:4.2.0') { + androidTestImplementation('com.adevinta.android:barista:4.3.0') { exclude group: 'org.jetbrains.kotlin' } androidTestImplementation libs.mockk.mockkAndroid From 59ac3b4f8b6c06d9a0bbd972bb257266dc78d199 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 24 Nov 2022 15:26:59 +0300 Subject: [PATCH 004/197] Update new strings of unverified sessions alert. --- library/ui-strings/src/main/res/values/strings.xml | 4 ++++ .../java/im/vector/app/features/home/HomeDetailFragment.kt | 4 ++-- .../java/im/vector/app/features/home/NewHomeDetailFragment.kt | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index f1d5bfbcad8..3945a80393d 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -2647,8 +2647,12 @@ Unencrypted Encrypted by an unverified device The authenticity of this encrypted message can\'t be guaranteed on this device. + Review where you’re logged in + Verify all your sessions to ensure your account & messages are safe + You have unverified sessions + Review to ensure your account is safe Verify the new login accessing your account: %1$s diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index e824dc18208..7552b934e41 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -256,8 +256,8 @@ class HomeDetailFragment : alertManager.postVectorAlert( VerificationVectorAlert( uid = uid, - title = getString(R.string.review_logins), - description = getString(R.string.verify_other_sessions), + title = getString(R.string.review_unverified_sessions_title), + description = getString(R.string.review_unverified_sessions_description), iconId = R.drawable.ic_shield_warning ).apply { viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer) diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt index 5956646eabe..62d7e58bdb8 100644 --- a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt @@ -270,8 +270,8 @@ class NewHomeDetailFragment : alertManager.postVectorAlert( VerificationVectorAlert( uid = uid, - title = getString(R.string.review_logins), - description = getString(R.string.verify_other_sessions), + title = getString(R.string.review_unverified_sessions_title), + description = getString(R.string.review_unverified_sessions_description), iconId = R.drawable.ic_shield_warning ).apply { viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer) From 821a5612358236a579a78b08296b9de2b347f4b7 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 25 Nov 2022 14:33:41 +0300 Subject: [PATCH 005/197] Add timeout preference for alert. --- .../src/main/java/im/vector/app/config/Config.kt | 4 ++++ .../java/im/vector/app/features/VectorFeatures.kt | 2 ++ .../app/features/settings/VectorPreferences.kt | 14 +++++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/vector-config/src/main/java/im/vector/app/config/Config.kt b/vector-config/src/main/java/im/vector/app/config/Config.kt index c91987dbfdf..fdc8e9f73b7 100644 --- a/vector-config/src/main/java/im/vector/app/config/Config.kt +++ b/vector-config/src/main/java/im/vector/app/config/Config.kt @@ -16,6 +16,8 @@ package im.vector.app.config +import kotlin.time.Duration.Companion.days + /** * Set of flags to configure the application. */ @@ -93,4 +95,6 @@ object Config { * Can be disabled by providing Analytics.Disabled */ val NIGHTLY_ANALYTICS_CONFIG = RELEASE_ANALYTICS_CONFIG.copy(sentryEnvironment = "NIGHTLY") + + val SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS = 7.days.inWholeMilliseconds // 1 Week } 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 95cf272abdd..28c2e379265 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -44,6 +44,7 @@ interface VectorFeatures { fun isQrCodeLoginForAllServers(): Boolean fun isReciprocateQrCodeLogin(): Boolean fun isVoiceBroadcastEnabled(): Boolean + fun isUnverifiedSessionsAlertEnabled(): Boolean } class DefaultVectorFeatures : VectorFeatures { @@ -63,4 +64,5 @@ class DefaultVectorFeatures : VectorFeatures { override fun isQrCodeLoginForAllServers(): Boolean = false override fun isReciprocateQrCodeLogin(): Boolean = false override fun isVoiceBroadcastEnabled(): Boolean = true + override fun isUnverifiedSessionsAlertEnabled(): Boolean = false } 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 447038d7683..3f350800572 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 @@ -245,6 +245,8 @@ class VectorPreferences @Inject constructor( // This key will be used to enable user for displaying live user info or not. const val SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO = "SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO" + const val SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS = "SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS" + // Possible values for TAKE_PHOTO_VIDEO_MODE const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0 const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1 @@ -1238,7 +1240,17 @@ class VectorPreferences @Inject constructor( fun setIpAddressVisibilityInDeviceManagerScreens(isVisible: Boolean) { defaultPrefs.edit { - putBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, isVisible) + putBoolean(SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, isVisible) + } + } + + fun getUnverifiedSessionsAlertLastShownMillis(): Long { + return defaultPrefs.getLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS, 0) + } + + fun setUnverifiedSessionsAlertLastShownMillis(lastShownMillis: Long) { + defaultPrefs.edit { + putLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS, lastShownMillis) } } } From 8835e4d25e8f0a3e55ef33cfe0d99834526e2561 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 25 Nov 2022 14:34:39 +0300 Subject: [PATCH 006/197] Create use case to decide to show alert. --- ...houldShowUnverifiedSessionsAlertUseCase.kt | 37 ++++++++++ ...dShowUnverifiedSessionsAlertUseCaseTest.kt | 74 +++++++++++++++++++ .../app/test/fakes/FakeVectorFeatures.kt | 4 + .../app/test/fakes/FakeVectorPreferences.kt | 4 + 4 files changed, 119 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt create mode 100644 vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt b/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt new file mode 100644 index 00000000000..0455b4399a1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home + +import im.vector.app.config.Config +import im.vector.app.core.time.Clock +import im.vector.app.features.VectorFeatures +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +class ShouldShowUnverifiedSessionsAlertUseCase @Inject constructor( + private val vectorFeatures: VectorFeatures, + private val vectorPreferences: VectorPreferences, + private val clock: Clock, +) { + + fun execute(): Boolean { + val isUnverifiedSessionsAlertEnabled = vectorFeatures.isUnverifiedSessionsAlertEnabled() + val unverifiedSessionsAlertLastShownMillis = vectorPreferences.getUnverifiedSessionsAlertLastShownMillis() + return isUnverifiedSessionsAlertEnabled && + clock.epochMillis() - unverifiedSessionsAlertLastShownMillis >= Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS + } +} diff --git a/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt new file mode 100644 index 00000000000..cb4b8b2a1f7 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.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.home + +import im.vector.app.config.Config +import im.vector.app.test.fakes.FakeClock +import im.vector.app.test.fakes.FakeVectorFeatures +import im.vector.app.test.fakes.FakeVectorPreferences +import org.amshove.kluent.shouldBe +import org.junit.Test + +private val AN_EPOCH = Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS + +class ShouldShowUnverifiedSessionsAlertUseCaseTest { + + private val fakeVectorFeatures = FakeVectorFeatures() + private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeClock = FakeClock() + + private val shouldShowUnverifiedSessionsAlertUseCase = ShouldShowUnverifiedSessionsAlertUseCase( + vectorFeatures = fakeVectorFeatures, + vectorPreferences = fakeVectorPreferences.instance, + clock = fakeClock, + ) + + @Test + fun `given the feature is disabled then the use case returns false`() { + fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(false) + fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(0L) + + shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe false + } + + @Test + fun `given the feature in enabled and there is not a saved preference then the use case returns true`() { + fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true) + fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(0L) + fakeClock.givenEpoch(AN_EPOCH + 1) + + shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe true + } + + @Test + fun `given the feature in enabled and last shown is a long time ago then the use case returns true`() { + fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true) + fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(AN_EPOCH) + fakeClock.givenEpoch(AN_EPOCH * 2 + 1) + + shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe true + } + + @Test + fun `given the feature in enabled and last shown is not a long time ago then the use case returns false`() { + fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true) + fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(AN_EPOCH) + fakeClock.givenEpoch(AN_EPOCH + 1) + + shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe false + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt index d989abc2146..c3c2fa684f8 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt @@ -50,4 +50,8 @@ class FakeVectorFeatures : VectorFeatures by spyk() { fun givenVoiceBroadcast(isEnabled: Boolean) { every { isVoiceBroadcastEnabled() } returns isEnabled } + + fun givenUnverifiedSessionsAlertEnabled(isEnabled: Boolean) { + every { isUnverifiedSessionsAlertEnabled() } returns isEnabled + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index d89764a77e2..101657b260b 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -56,4 +56,8 @@ class FakeVectorPreferences { fun givenSessionManagerShowIpAddress(showIpAddress: Boolean) { every { instance.showIpAddressInSessionManagerScreens() } returns showIpAddress } + + fun givenUnverifiedSessionsAlertLastShownMillis(lastShownMillis: Long) { + every { instance.getUnverifiedSessionsAlertLastShownMillis() } returns lastShownMillis + } } From d7dc5c812e3e7132bfeb34fe090e9f80b6bc89ed Mon Sep 17 00:00:00 2001 From: gradle-update-robot Date: Sun, 27 Nov 2022 00:26:10 +0000 Subject: [PATCH 007/197] Update Gradle Wrapper from 7.5.1 to 7.6. Signed-off-by: gradle-update-robot --- gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 61574 bytes gradle/wrapper/gradle-wrapper.properties | 5 +++-- gradlew | 12 ++++++++---- gradlew.bat | 1 + 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..943f0cbfa754578e88a3dae77fce6e3dea56edbf 100644 GIT binary patch delta 36524 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%0DGK?7GHTj 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 VwlMN}`%er%aGR6olj~j${vQ;P=LY}) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f7189a776c1..bc073f67610 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +distributionSha256Sum=312eb12875e1747e05c2f81a4789902d7e4ec5defbd1eefeaccc08acf096505d +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index a69d9cb6c20..65dcd68d65c 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,10 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' @@ -143,12 +143,16 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac diff --git a/gradlew.bat b/gradlew.bat index 53a6b238d41..6689b85beec 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% From d04e2b0f8264346b64360017fc6bf77e91a2d3dd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 28 Nov 2022 12:40:17 +0100 Subject: [PATCH 008/197] Trigger CI From c5fe9d4a18b3898b976b3431ce2093929d35128e Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Tue, 29 Nov 2022 15:07:51 +0000 Subject: [PATCH 009/197] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2556 of 2556 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/pt_BR/ --- library/ui-strings/src/main/res/values-pt-rBR/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml index 8baba5df534..8129a234fbe 100644 --- a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml +++ b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml @@ -2723,7 +2723,7 @@ Sessões não-verificadas Sessões inativas são sessões que você não tem usado em algum tempo, mas elas continuam a receber chaves de encriptação. \n -\nRemover sessões inativas melhora segurança e performance, e torna-o mais fácil para você identificar se uma nova sessão é suspeita. +\nRemover sessões inativas melhora segurança e performance, e torna mais fácil para você identificar se uma nova sessão é suspeita. Sessões inativas Por favor esteja ciente que nomes de sessões também são visíveis a pessoas com quem você se comunica. Nomes de sessões personalizadas podem ajudar você a reconhecer seus dispositivos mais facilmente. @@ -2844,9 +2844,9 @@ Não dá pra começar um novo broadcast de voz Avançar rápido 30 segundos Retroceder 30 segundos - Sessões verificadas são onde quer que você está usando esta conta depois de entrar sua frasepasse ou confirmar sua identidade com uma outra sessão verificada. + Sessões verificadas são onde quer que você esteja usando esta conta depois de entrar sua frasepasse ou confirmar sua identidade com uma outra sessão verificada. \n -\nIsto significa que você tem todas as chaves necessitadas para destrancar suas mensagens encriptadas e confirmar a outras(os) usuárias(os) que você confia nesta sessão. +\nIsto significa que você tem todas as chaves necessárias para destrancar suas mensagens encriptadas e confirmar a outras(os) usuárias(os) que você confia nesta sessão. Fazer signout de %1$d sessão Fazer signout de %1$d sessões From 1c0fe56329f828aab086dba0f814b5cadc71f11f Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Mon, 28 Nov 2022 21:32:51 +0000 Subject: [PATCH 010/197] Translated using Weblate (Swedish) Currently translated at 100.0% (2556 of 2556 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sv/ --- .../ui-strings/src/main/res/values-sv/strings.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/library/ui-strings/src/main/res/values-sv/strings.xml b/library/ui-strings/src/main/res/values-sv/strings.xml index 65318096c74..45cfe4338b5 100644 --- a/library/ui-strings/src/main/res/values-sv/strings.xml +++ b/library/ui-strings/src/main/res/values-sv/strings.xml @@ -2852,4 +2852,18 @@ Kan inte starta en ny röstsändning Spola framåt 30 sekunder Spola tillbaka 30 sekunder + skickade en omröstning. + skickade en dekal. + skickade en video. + skickade en bild. + skickade ett röstmeddelande. + skickade en ljudfil. + skickade en fil. + Svar på + Dölj IP-adress + Visa IP-adress + %1$s kvar + Citerar + Besvarar %s + Redigerar \ No newline at end of file From b3ffc4d76cf09f3cdeb688a17ad4c08ba7fc86d5 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Mon, 28 Nov 2022 01:47:14 +0000 Subject: [PATCH 011/197] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2556 of 2556 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/uk/ --- .../ui-strings/src/main/res/values-uk/strings.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml index 19889892ffc..d65a7a1da7d 100644 --- a/library/ui-strings/src/main/res/values-uk/strings.xml +++ b/library/ui-strings/src/main/res/values-uk/strings.xml @@ -2966,16 +2966,16 @@ Вийти Залишилося %1$s надсилає аудіофайл. - відправив файл. + надсилає файл. У відповідь на Сховати IP-адресу - створив голосування. - відправив наліпку. - відправив відео. - відправив зображення. - відправив голосове повідомлення. + створює опитування. + надсилає наліпку. + надсилає відео. + надсилає зображення. + надсилає голосове повідомлення. Показати IP-адресу Цитуючи - У відповідь на %s + У відповідь %s Редагування \ No newline at end of file From 4a70ea851814ddb394ecd091b620bc1062323e2a Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Mon, 28 Nov 2022 21:24:35 +0000 Subject: [PATCH 012/197] Translated using Weblate (Swedish) Currently translated at 100.0% (82 of 82 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/sv/ --- fastlane/metadata/android/sv-SE/changelogs/40105080.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/sv-SE/changelogs/40105080.txt diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105080.txt b/fastlane/metadata/android/sv-SE/changelogs/40105080.txt new file mode 100644 index 00000000000..cee589ed356 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40105080.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: buggfixar och förbättringar. +Full ändringslogg: https://github.com/vector-im/element-android/releases From b58050f4969c867e8f7ce93924c6372e18fba151 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Nov 2022 09:13:47 +0000 Subject: [PATCH 013/197] Bump kotlin-reflect from 1.7.21 to 1.7.22 (#7665) Bumps [kotlin-reflect](https://github.com/JetBrains/kotlin) from 1.7.21 to 1.7.22. - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.7.21...v1.7.22) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-reflect dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- vector-app/build.gradle | 2 +- vector/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 86b94a8497c..be67e5ff43c 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -402,7 +402,7 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator androidTestImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22" debugImplementation libs.androidx.fragmentTesting debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' } diff --git a/vector/build.gradle b/vector/build.gradle index 890236422e8..1a5179f07b0 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -331,5 +331,5 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator debugImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22" } From a73fe9585f0ddcc73370d7caced947e628a28072 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Nov 2022 09:15:18 +0000 Subject: [PATCH 014/197] Bump danger/danger-js from 11.1.4 to 11.2.0 (#7584) Bumps [danger/danger-js](https://github.com/danger/danger-js) from 11.1.4 to 11.2.0. - [Release notes](https://github.com/danger/danger-js/releases) - [Changelog](https://github.com/danger/danger-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/danger/danger-js/compare/11.1.4...11.2.0) --- updated-dependencies: - dependency-name: danger/danger-js dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/danger.yml | 2 +- .github/workflows/quality.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 30b6600c940..e5226d07235 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -11,7 +11,7 @@ jobs: - run: | npm install --save-dev @babel/plugin-transform-flow-strip-types - name: Danger - uses: danger/danger-js@11.1.4 + uses: danger/danger-js@11.2.0 with: args: "--dangerfile tools/danger/dangerfile.js" env: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 9d9e8e76e8e..57dd5a6a457 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -66,7 +66,7 @@ jobs: yarn add danger-plugin-lint-report --dev - name: Danger lint if: always() - uses: danger/danger-js@11.1.4 + uses: danger/danger-js@11.2.0 with: args: "--dangerfile tools/danger/dangerfile-lint.js" env: From 52477aa9d57527b707680716c33b9c230ecf1a39 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Nov 2022 11:03:58 +0100 Subject: [PATCH 015/197] version++ --- matrix-sdk-android/build.gradle | 2 +- vector-app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 60b0329fbc8..4be8d55614b 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -62,7 +62,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.5.10\"" + buildConfigField "String", "SDK_VERSION", "\"1.5.12\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index be67e5ff43c..fb80010d17b 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 5 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 10 +ext.versionPatch = 12 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' From 8cf8852aae5d9ab500395a5572dc1976c3165e56 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Nov 2022 11:50:27 +0100 Subject: [PATCH 016/197] Release script: Update last part of the script --- tools/release/releaseScript.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh index d8980b9da7b..3abe8a46f18 100755 --- a/tools/release/releaseScript.sh +++ b/tools/release/releaseScript.sh @@ -225,12 +225,13 @@ else fi printf "\n================================================================================\n" -read -p "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Amain to build the 'main' branch. Press enter when it's done." +printf "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Amain to build the 'main' branch.\n" +read -p "After GHA is finished, please enter the artifact URL (for 'vector-gplay-release-unsigned'): " artifactUrl printf "\n================================================================================\n" printf "Running the release script...\n" cd ${releaseScriptLocation} -${releaseScriptFullPath} "v${version}" +${releaseScriptFullPath} "v${version}" ${artifactUrl} cd - printf "\n================================================================================\n" From 714f8b3a7512cb82c07c655eae1baa3775d577a5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Nov 2022 12:01:30 +0100 Subject: [PATCH 017/197] Release script: Improve release script again. --- tools/release/releaseScript.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh index 3abe8a46f18..fa5afc6faa1 100755 --- a/tools/release/releaseScript.sh +++ b/tools/release/releaseScript.sh @@ -235,8 +235,8 @@ ${releaseScriptFullPath} "v${version}" ${artifactUrl} cd - printf "\n================================================================================\n" +read -p "Installing apk on a real device, press enter when a real device is connected. " apkPath="${releaseScriptLocation}/Element/v${version}/vector-gplay-arm64-v8a-release-signed.apk" -printf "Installing apk on a real device...\n" adb -d install ${apkPath} read -p "Please run the APK on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done." From 0868686caadf3551a3ac0e42e19db1601ef3b748 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Nov 2022 12:52:39 +0100 Subject: [PATCH 018/197] Release script: Send message to Android Room --- tools/release/releaseScript.sh | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh index fa5afc6faa1..83572d4910b 100755 --- a/tools/release/releaseScript.sh +++ b/tools/release/releaseScript.sh @@ -246,9 +246,26 @@ read -p "Create the release on gitHub from the tag https://github.com/vector-im/ read -p "Add the 4 signed APKs to the GitHub release. Press enter when it's done." printf "\n================================================================================\n" -printf "Ping the Android Internal room. Here is an example of message which can be sent:\n\n" -printf "@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!\n\n" -read -p "Press enter when it's done." +printf "Message for the Android internal room:\n\n" +message="@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!" +printf "${message}\n\n" + +matrixOrgToken="${MATRIX_ORG_TOKEN}" +if [[ -z "${matrixOrgToken}" ]]; then + read -p "MATRIX_ORG_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter when it's done " +else + read -p "Send this message to the room (yes/no) default to yes? " doSend + doSend=${doSend:-yes} + if [ ${doSend} == "yes" ]; then + printf "Sending message...\n" + transactionId=`openssl rand -hex 16` + # Element Android internal + matrixRoomId="!LiSLXinTDCsepePiYW:matrix.org" + curl -X PUT --data $"{\"msgtype\":\"m.text\",\"body\":\"${message}\"}" -H "Authorization: Bearer ${matrixOrgToken}" https://matrix-client.matrix.org/_matrix/client/r0/rooms/${matrixRoomId}/send/m.room.message/\$local.${transactionId} + else + printf "Message not sent, please send it manually!\n" + fi +fi printf "\n================================================================================\n" printf "Congratulation! Kudos for using this script! Have a nice day!\n" From b699e9db3a71b4aa0182e41895a99b2c82831d19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Nov 2022 13:58:28 +0100 Subject: [PATCH 019/197] Bump sentry-android from 6.7.0 to 6.9.0 (#7668) Bumps [sentry-android](https://github.com/getsentry/sentry-java) from 6.7.0 to 6.9.0. - [Release notes](https://github.com/getsentry/sentry-java/releases) - [Changelog](https://github.com/getsentry/sentry-java/blob/6.9.0/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-java/compare/6.7.0...6.9.0) --- updated-dependencies: - dependency-name: io.sentry:sentry-android dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 31c32bb26b5..51f8f9df0d3 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -26,7 +26,7 @@ def jjwt = "0.11.5" // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert // the whole commit which set version 0.16.0-SNAPSHOT def vanniktechEmoji = "0.16.0-SNAPSHOT" -def sentry = "6.7.0" +def sentry = "6.9.0" def fragment = "1.5.4" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 From d1cef1bc5c56fc804fdfb9059d489ca43dbbe13f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Nov 2022 23:02:20 +0000 Subject: [PATCH 020/197] Bump com.autonomousapps.dependency-analysis from 1.16.0 to 1.17.0 Bumps com.autonomousapps.dependency-analysis from 1.16.0 to 1.17.0. --- updated-dependencies: - dependency-name: com.autonomousapps.dependency-analysis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 51604b67a82..58084ab64d1 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ plugins { id "com.google.devtools.ksp" version "1.7.21-1.0.8" // Dependency Analysis - id 'com.autonomousapps.dependency-analysis' version "1.16.0" + id 'com.autonomousapps.dependency-analysis' version "1.17.0" // Gradle doctor id "com.osacky.doctor" version "0.8.1" } From e4212bd7dbcfdbb4c99ba237cc7e6c91937920e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Nov 2022 23:03:13 +0000 Subject: [PATCH 021/197] Bump flipper from 0.174.0 to 0.175.0 Bumps `flipper` from 0.174.0 to 0.175.0. Updates `flipper` from 0.174.0 to 0.175.0 - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.174.0...v0.175.0) Updates `flipper-network-plugin` from 0.174.0 to 0.175.0 - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.174.0...v0.175.0) --- updated-dependencies: - dependency-name: com.facebook.flipper:flipper dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.facebook.flipper:flipper-network-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 51f8f9df0d3..cb10170e939 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -17,7 +17,7 @@ def markwon = "4.6.2" def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.174.0" +def flipper = "0.175.0" def epoxy = "5.0.0" def mavericks = "3.0.1" def glide = "4.14.2" From 279756bdfb64ab1c1df07be594313d0ae3ae334d Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Thu, 1 Dec 2022 05:59:21 +0000 Subject: [PATCH 022/197] Translated using Weblate (Czech) Currently translated at 100.0% (2558 of 2558 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/cs/ --- library/ui-strings/src/main/res/values-cs/strings.xml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml index f260a129fcd..67cc3353aa5 100644 --- a/library/ui-strings/src/main/res/values-cs/strings.xml +++ b/library/ui-strings/src/main/res/values-cs/strings.xml @@ -2789,7 +2789,7 @@ Pravost této šifrované zprávy nelze v tomto zařízení zaručit. Požadujte, aby klávesnice neaktualizovala žádné personalizované údaje, jako je historie psaní a slovník, na základě toho, co jste napsali v konverzacích. Upozorňujeme, že některé klávesnice nemusí toto nastavení respektovat. Inkognito klávesnice - Přidá znaky (╯°□°)╯︵ ┻━┻ před zprávy ve formátu obyčejného textu + Přidá znaky (╯°□°)╯︵ ┻━┻ před zprávy ve formátu prostého textu Hlasové vysílání Otevřít nástroje pro vývojáře 🔒 V nastavení zabezpečení jste povolili šifrování pouze do ověřených relací pro všechny místnosti. @@ -2824,8 +2824,8 @@ ${app_name} potřebuje oprávnění k zobrazování oznámení. Oznámení mohou zobrazovat vaše zprávy, pozvánky atd. \n \nPro zobrazování oznámení povolte přístup na dalších vyskakovacích oknech. - Vyzkoušejte rozšířený textový editor (textový režim již brzy) - Povolit rozšířený textový editor + Vyzkoušejte editor formátovaného textu (režim prostého textu již brzy) + Povolit editor formátovaného textu Ujistěte se, že znáte původ tohoto kódu. Propojením zařízení poskytnete někomu plný přístup ke svému účtu. Potvrdit Zkuste to znovu @@ -2868,7 +2868,7 @@ Druhé zařízení je již přihlášeno. Při nastavování zabezpečeného zasílání zpráv se vyskytl problém se zabezpečením. Může být napadena jedna z následujících věcí: váš domovský server; vaše internetové připojení; vaše zařízení; Žádost se nezdařila. - Ukládání do vyrovnávací paměti + Ukládání do vyrovnávací paměti… Pozastavit hlasové vysílání Přehrát nebo obnovit hlasové vysílání Ukončit záznam hlasového vysílání @@ -2922,4 +2922,6 @@ Citace Odpovídám na %s Úpravy + Zobrazit poslední chaty v nabídce sdílení systému + Povolit přímé sdílení \ No newline at end of file From a0528fe0ce203f5ac1bfe4f012a636daa0536bb8 Mon Sep 17 00:00:00 2001 From: Vri Date: Wed, 30 Nov 2022 06:31:56 +0000 Subject: [PATCH 023/197] Translated using Weblate (German) Currently translated at 100.0% (2558 of 2558 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/de/ --- library/ui-strings/src/main/res/values-de/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml index be53c15026f..809ee477fcd 100644 --- a/library/ui-strings/src/main/res/values-de/strings.xml +++ b/library/ui-strings/src/main/res/values-de/strings.xml @@ -2815,7 +2815,7 @@ Die Anfrage ist fehlgeschlagen. Abspielen oder fortsetzen der Sprachübertragung Fortsetzen der Sprachübertragung - Puffere + Puffere … Pausiere Sprachübertragung Stoppe Aufzeichnung der Sprachübertragung Pausiere Aufzeichnung der Sprachübertragung @@ -2865,4 +2865,6 @@ %s antworten IP-Adresse ausblenden IP-Adresse anzeigen + Kürzliche Unterhaltungen im Teilen-Menü des Systems anzeigen + Direktes Teilen aktivieren \ No newline at end of file From c6ed280a6f0a65b09cb63a8638afd3205fd2ccaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 30 Nov 2022 08:14:33 +0000 Subject: [PATCH 024/197] Translated using Weblate (Estonian) Currently translated at 99.6% (2550 of 2558 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/et/ --- library/ui-strings/src/main/res/values-et/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-et/strings.xml b/library/ui-strings/src/main/res/values-et/strings.xml index 156221379d4..96d9650ceb0 100644 --- a/library/ui-strings/src/main/res/values-et/strings.xml +++ b/library/ui-strings/src/main/res/values-et/strings.xml @@ -2805,7 +2805,7 @@ Teine seade on juba võrku loginud. Turvalise sõnumivahetuse ülesseadmisel tekkis turvaviga. Üks kolmest võib olla sattunud vale osapoole kontrolli alla: sinu koduserver, sinu internetiühendus või sinu seade; Päring ei õnnestunud. - Andmed on puhverdamisel + Andmed on puhverdamisel… Alusta või jätka ringhäälingukõne esitamist Lõpeta ringhäälingukõne salvestamine Peata ringhäälingukõne salvestamine @@ -2857,4 +2857,6 @@ saatis video. saatis kleepsu. koostas küsitluse. + Kasuta otsejagamist + Näita viimaseid vestlusi süsteemses jagamisvaates \ No newline at end of file From 27acb198ab9c760e4ec348c153abc9a088dd1f0d Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Wed, 30 Nov 2022 11:24:07 +0000 Subject: [PATCH 025/197] Translated using Weblate (Persian) Currently translated at 99.7% (2551 of 2558 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fa/ --- library/ui-strings/src/main/res/values-fa/strings.xml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/library/ui-strings/src/main/res/values-fa/strings.xml b/library/ui-strings/src/main/res/values-fa/strings.xml index cc8d60a87b3..a3a74df10f2 100644 --- a/library/ui-strings/src/main/res/values-fa/strings.xml +++ b/library/ui-strings/src/main/res/values-fa/strings.xml @@ -943,7 +943,7 @@ \n \nپیام‌هایتان با قفل‌هایی امن شده‌اند و فقط شما و گیرندگان دیگر، کلیدهای یکتا را برای قفل‌گشاییشان دارید. امنیت - بثیش‌تر بدانید + بیش‌تر بدانید بیش‌تر کنش‌های مدیر تنظمیات اتاق @@ -2783,7 +2783,7 @@ نظرسنجی‌ها پیوست‌ها برچسب‌ها - میانگیری + میانگیری… زنده تأیید ۳ @@ -2844,4 +2844,9 @@ نقل کردن پاسخ دادن به %s ویرایش کردن + می‌توانید با یک رمز QR از این افزاره برای ورود به افزاره‌ای همراه یا روی وب استفاده کنید. دو راه برای این کار وجود دارد: + مشکلی امنیتی در برپایی پیام‌رسانی امن وجود داشت. ممکن است یکی از موارد زیر دستکاری شده باشند: کارساز خانیگیتان؛ اتّصال اینترنتیتان؛ افزاره(های)تان؛ + لطفاً مطمئن شوید که مبدأ این کد را می‌دانید. با پیوند دادن افزاره‌ها، دسترسی کامل را به حسابتان می‌دهید. + نمایش گپ‌های اخیر در فهرست هم رسانی سامانه + به کار انداختن هم‌رسانی مستقیم \ No newline at end of file From 1c7f789928f3414be104f934cdad76c25a9cd25b Mon Sep 17 00:00:00 2001 From: Glandos Date: Wed, 30 Nov 2022 11:55:22 +0000 Subject: [PATCH 026/197] Translated using Weblate (French) Currently translated at 100.0% (2558 of 2558 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fr/ --- library/ui-strings/src/main/res/values-fr/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml index cf49733bdfa..d74d3bac711 100644 --- a/library/ui-strings/src/main/res/values-fr/strings.xml +++ b/library/ui-strings/src/main/res/values-fr/strings.xml @@ -2814,7 +2814,7 @@ Vous pouvez utiliser cet appareil pour connecter un appareil mobile ou un client web avec un QR code. Il y a deux façons de le faire : Se connecter avec un QR code Scanner le QR code - Mise en mémoire tampon + Mise en mémoire tampon… Mettre en pause la diffusion audio Lire ou continuer la diffusion audio Arrêter l’enregistrement de la diffusion audio @@ -2866,4 +2866,6 @@ Citation de Réponse à %s Modification + Affiche les conversations récentes dans le menu de partage du système + Activer le partage direct \ No newline at end of file From ab1db4de6870537774b8153dd82d28cc5153bca4 Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 29 Nov 2022 23:32:26 +0000 Subject: [PATCH 027/197] Translated using Weblate (Indonesian) Currently translated at 100.0% (2558 of 2558 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/id/ --- library/ui-strings/src/main/res/values-in/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml index ce9e5240672..da4c4746892 100644 --- a/library/ui-strings/src/main/res/values-in/strings.xml +++ b/library/ui-strings/src/main/res/values-in/strings.xml @@ -2762,7 +2762,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Permintaan gagal. Memungkinkan untuk merekam dan mengirim siaran suara dalam linimasa ruangan. Aktifkan siaran suara (dalam pengembangan aktif) - Memuat + Memuat… Jeda siaran suara Mainkan atau lanjutkan siaran suara Hentikan rekaman siaran suara @@ -2812,4 +2812,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Mengedit Tampilkan alamat IP Membalas ke %s + Tampilkan obrolan terkini dalam menu pembagian sistem + Aktifkan pembagian langsung \ No newline at end of file From 05e6a59a867664d9a35c3d5927ed22bced059a79 Mon Sep 17 00:00:00 2001 From: random Date: Thu, 1 Dec 2022 09:11:39 +0000 Subject: [PATCH 028/197] Translated using Weblate (Italian) Currently translated at 100.0% (2558 of 2558 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/it/ --- library/ui-strings/src/main/res/values-it/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-it/strings.xml b/library/ui-strings/src/main/res/values-it/strings.xml index d244f26a435..d6a7858ebc7 100644 --- a/library/ui-strings/src/main/res/values-it/strings.xml +++ b/library/ui-strings/src/main/res/values-it/strings.xml @@ -2805,7 +2805,7 @@ L\'altro dispositivo ha già fatto l\'accesso. Si è verificato un problema di sicurezza configurando i messaggi sicuri. Una delle seguenti cose potrebbe essere compromessa: il tuo homeserver; la/e connessione/i internet; il/i dispositivo/i; La richiesta è fallita. - Buffering + Buffer… Sospendi trasmissione vocale Avvia o riprendi trasmissione vocale Ferma registrazione trasmissione vocale @@ -2857,4 +2857,6 @@ Citazione Risposta a %s Modifica + Mostra chat recenti nel menu di condivisione di sistema + Attiva condivisione diretta \ No newline at end of file From 3d84a999e06d015631613542d008ab26753d5eea Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Wed, 30 Nov 2022 09:50:08 +0000 Subject: [PATCH 029/197] Translated using Weblate (Slovak) Currently translated at 100.0% (2558 of 2558 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/ --- library/ui-strings/src/main/res/values-sk/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml index 078ffc44ebb..f59073c5db2 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -2868,7 +2868,7 @@ Žiadosť zlyhala. Možnosť nahrávania a odosielania hlasového vysielania v časovej osi miestnosti. Zapnúť hlasové vysielanie (v štádiu aktívneho vývoja) - Načítavanie do vyrovnávacej pamäte + Načítavanie do vyrovnávacej pamäte… Pozastaviť hlasové vysielanie Prehrať alebo pokračovať v nahrávaní hlasového vysielania Zastaviť nahrávanie hlasového vysielania @@ -2922,4 +2922,6 @@ Zobraziť IP adresu Odpoveď na %s Úprava + Zobraziť posledné konverzácie v systémovej ponuke zdieľania + Povoliť priame zdieľanie \ No newline at end of file From b759f40c13eb1ba23a7e5ad5d5ebbaeb6dc18cce Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 30 Nov 2022 16:23:19 +0000 Subject: [PATCH 030/197] Translated using Weblate (Albanian) Currently translated at 99.3% (2541 of 2558 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sq/ --- library/ui-strings/src/main/res/values-sq/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml index 800ec17dcfe..58214e8a224 100644 --- a/library/ui-strings/src/main/res/values-sq/strings.xml +++ b/library/ui-strings/src/main/res/values-sq/strings.xml @@ -2659,7 +2659,7 @@ \nKy shërbyes Home mund të mos jetë formësuar të shfaqë harta. Përfundimet do të jenë të dukshme pasi të ketë përfunduar pyetësori Kur bëhet ftesë në një dhomë të fshehtëzuar që ka historik ndarjesh me të tjerët, historiku i fshehtëzuar do të jetë i dukshëm. - Përdo + Ndal transmetim zanor Luani ose vazhdoni luajtje transmetimi zanor Ndal incizim transmetimi zanor @@ -2851,4 +2851,6 @@ Kthim prapa 30 sekonda Si përgjigje për %s Aktivizo MD të lënë për më vonë + Tkurr pjella të %s + Zgjero pjella të %s \ No newline at end of file From 571d1a4816652f6a46b1b7eced2ef6625c4f5e1a Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 29 Nov 2022 22:10:18 +0000 Subject: [PATCH 031/197] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2558 of 2558 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/uk/ --- library/ui-strings/src/main/res/values-uk/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml index d65a7a1da7d..6a1c5355ab0 100644 --- a/library/ui-strings/src/main/res/values-uk/strings.xml +++ b/library/ui-strings/src/main/res/values-uk/strings.xml @@ -2922,7 +2922,7 @@ Запит не виконаний. Можливість записувати та надсилати голосові трансляції до стрічки кімнати. Увімкнути голосові трансляції (в активній розробці) - Буферизація + Буферизація… Призупинити голосову трансляцію Відтворити або поновити відтворення голосової трансляції Припинити запис голосової трансляції @@ -2978,4 +2978,6 @@ Цитуючи У відповідь %s Редагування + Показувати останні бесіди в системному меню загального доступу + Увімкнути пряме поширення \ No newline at end of file From a57162cf83aa03ec8959f1585a76621a27b04fde Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 30 Nov 2022 02:05:55 +0000 Subject: [PATCH 032/197] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2558 of 2558 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hant/ --- library/ui-strings/src/main/res/values-zh-rTW/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml index dc5f6d85e35..9a5439b2aea 100644 --- a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml @@ -2760,7 +2760,7 @@ 請求失敗。 可以在聊天室時間軸中錄製並傳送語音廣播。 啟用語音廣播(正在積極開發中) - 正在緩衝 + 正在緩衝…… 暫停語音廣播 播放或繼續語音廣播 停止語音廣播錄製 @@ -2810,4 +2810,6 @@ 引用 回覆給 %s 正在編輯 + 在系統分享選單中顯示最近聊天 + 啟用直接分享 \ No newline at end of file From 56715f13d48d31bf6df88e07fb31c29d99d54ee3 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 30 Nov 2022 16:24:03 +0000 Subject: [PATCH 033/197] Translated using Weblate (Albanian) Currently translated at 100.0% (82 of 82 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/sq/ --- fastlane/metadata/android/sq/changelogs/40105080.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/sq/changelogs/40105080.txt diff --git a/fastlane/metadata/android/sq/changelogs/40105080.txt b/fastlane/metadata/android/sq/changelogs/40105080.txt new file mode 100644 index 00000000000..b059e86cbda --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105080.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: ndreqje të metash dhe përmirësime. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases From 0c11778d3354387de0f6762bb3590855318648b9 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 1 Dec 2022 11:26:55 +0100 Subject: [PATCH 034/197] Rich Text Editor: fix several inset issues in room screen (#7681) --- changelog.d/7680.bugfix | 3 +++ .../utils/ExpandingBottomSheetBehavior.kt | 23 ++++++++++++------- .../composer/MessageComposerFragment.kt | 2 +- .../src/main/res/layout/fragment_timeline.xml | 2 +- 4 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 changelog.d/7680.bugfix diff --git a/changelog.d/7680.bugfix b/changelog.d/7680.bugfix new file mode 100644 index 00000000000..2e3b4b2e48a --- /dev/null +++ b/changelog.d/7680.bugfix @@ -0,0 +1,3 @@ +Rich Text Editor: fix several issues related to insets: +* Empty space displayed at the bottom when you don't have permissions to send messages into a room. +* Wrong insets being kept when you exit the room screen and the keyboard is displayed, then come back to it. diff --git a/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt b/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt index 0474cdea7ea..47326bca767 100644 --- a/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt +++ b/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt @@ -608,26 +608,33 @@ class ExpandingBottomSheetBehavior : CoordinatorLayout.Behavior { initialPaddingBottom = view.paddingBottom // This should only be used to set initial insets and other edge cases where the insets can't be applied using an animation. - var applyInsetsFromAnimation = false + var isAnimating = false - // This will animated inset changes, making them look a lot better. However, it won't update initial insets. + // This will animate inset changes, making them look a lot better. However, it won't update initial insets. ViewCompat.setWindowInsetsAnimationCallback(view, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) { + override fun onPrepare(animation: WindowInsetsAnimationCompat) { + isAnimating = true + } + override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList): WindowInsetsCompat { - return applyInsets(view, insets) + return if (isAnimating) { + applyInsets(view, insets) + } else { + insets + } } override fun onEnd(animation: WindowInsetsAnimationCompat) { - applyInsetsFromAnimation = false + isAnimating = false view.requestApplyInsets() } }) ViewCompat.setOnApplyWindowInsetsListener(view) { _: View, insets: WindowInsetsCompat -> - if (!applyInsetsFromAnimation) { - applyInsetsFromAnimation = true - applyInsets(view, insets) - } else { + if (isAnimating) { insets + } else { + applyInsets(view, insets) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index d551850ff36..97e74785ec4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -255,7 +255,7 @@ class MessageComposerFragment : VectorBaseFragment(), A ) { mainState, messageComposerState, attachmentState -> if (mainState.tombstoneEvent != null) return@withState - (composer as? View)?.isInvisible = !messageComposerState.isComposerVisible + (composer as? View)?.isVisible = messageComposerState.isComposerVisible composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible (composer as? RichTextComposerLayout)?.isTextFormattingEnabled = attachmentState.isTextFormattingEnabled } diff --git a/vector/src/main/res/layout/fragment_timeline.xml b/vector/src/main/res/layout/fragment_timeline.xml index 6597b464acb..6e83dbe8fd0 100644 --- a/vector/src/main/res/layout/fragment_timeline.xml +++ b/vector/src/main/res/layout/fragment_timeline.xml @@ -75,7 +75,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:overScrollMode="always" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/notificationAreaView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" From da5db0ed15f68bfeb573ceac96380b26fcbc290e Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Thu, 1 Dec 2022 13:39:01 +0000 Subject: [PATCH 035/197] [Rich text editor] Fix keyboard closing after collapsing rich text editor (#7659) --- changelog.d/7659.bugfix | 1 + .../home/room/detail/composer/RichTextComposerLayout.kt | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) create mode 100644 changelog.d/7659.bugfix diff --git a/changelog.d/7659.bugfix b/changelog.d/7659.bugfix new file mode 100644 index 00000000000..38be1008ef7 --- /dev/null +++ b/changelog.d/7659.bugfix @@ -0,0 +1 @@ +[Rich text editor] Fix keyboard closing after collapsing editor diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index a058d4fdaa7..48459b5c062 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -42,7 +42,6 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import com.google.android.material.shape.MaterialShapeDrawable import im.vector.app.R -import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.setTextIfDifferent import im.vector.app.core.extensions.showKeyboard import im.vector.app.core.utils.DimensionConverter @@ -132,8 +131,6 @@ class RichTextComposerLayout @JvmOverloads constructor( views.bottomSheetHandle.isVisible = isFullScreen if (isFullScreen) { editText.showKeyboard(true) - } else { - editText.hideKeyboard() } this.isFullScreen = isFullScreen } From 341967bf3c31e0f247e6081f5ba0f6e5b32ec523 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 1 Dec 2022 15:25:54 +0100 Subject: [PATCH 036/197] Fix crash when invalid url is entered #7672 --- .../onboarding/OnboardingViewModel.kt | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) 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 022fea5ed1e..7fe73f8087f 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 @@ -118,7 +118,7 @@ class OnboardingViewModel @AssistedInject constructor( } } - private fun checkQrCodeLoginCapability(homeServerUrl: String) { + private suspend fun checkQrCodeLoginCapability(config: HomeServerConnectionConfig) { if (!vectorFeatures.isQrCodeLoginEnabled()) { setState { copy( @@ -133,16 +133,12 @@ class OnboardingViewModel @AssistedInject constructor( ) } } else { - viewModelScope.launch { - // check if selected server supports MSC3882 first - homeServerConnectionConfigFactory.create(homeServerUrl)?.let { - val canLoginWithQrCode = authenticationService.isQrLoginSupported(it) - setState { - copy( - canLoginWithQrCode = canLoginWithQrCode - ) - } - } + // check if selected server supports MSC3882 first + val canLoginWithQrCode = authenticationService.isQrLoginSupported(config) + setState { + copy( + canLoginWithQrCode = canLoginWithQrCode + ) } } } @@ -710,7 +706,6 @@ class OnboardingViewModel @AssistedInject constructor( _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) } else { startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction) - checkQrCodeLoginCapability(homeServerConnectionConfig.homeServerUri.toString()) } } @@ -769,6 +764,8 @@ class OnboardingViewModel @AssistedInject constructor( _viewEvents.post(OnboardingViewEvents.OutdatedHomeserver) } + checkQrCodeLoginCapability(config) + when (trigger) { is OnboardingAction.HomeServerChange.SelectHomeServer -> { onHomeServerSelected(config, serverTypeOverride, authResult) From d96ff6e5277b619a637bc2403889a285390bb191 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 1 Dec 2022 15:38:31 +0100 Subject: [PATCH 037/197] Changelog --- changelog.d/7684.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7684.bugfix diff --git a/changelog.d/7684.bugfix b/changelog.d/7684.bugfix new file mode 100644 index 00000000000..4a9af884a11 --- /dev/null +++ b/changelog.d/7684.bugfix @@ -0,0 +1 @@ + Fix crash when invalid homeserver url is entered. From d580d4cdb6b2a354ebade02450ce806acd792b4b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 1 Dec 2022 16:25:40 +0100 Subject: [PATCH 038/197] Read sensible data from the env and do not rely to an external script anymore. --- tools/release/releaseScript.sh | 154 ++++++++++++++++++++++++++++----- 1 file changed, 133 insertions(+), 21 deletions(-) diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh index 83572d4910b..d76cd980619 100755 --- a/tools/release/releaseScript.sh +++ b/tools/release/releaseScript.sh @@ -19,43 +19,81 @@ # Ignore any error to not stop the script set +e -printf "\n" -printf "================================================================================\n" +printf "\n================================================================================\n" printf "| Welcome to the release script! |\n" printf "================================================================================\n" -releaseScriptLocation="${RELEASE_SCRIPT_PATH}" +printf "Checking environment...\n" +envError=0 -if [[ -z "${releaseScriptLocation}" ]]; then - printf "Fatal: RELEASE_SCRIPT_PATH is not defined in the environment. Please set to the path of your local file 'releaseElement2.sh'.\n" - exit 1 +# Path of the key store (it's a file) +keyStorePath="${ELEMENT_KEYSTORE_PATH}" +if [[ -z "${keyStorePath}" ]]; then + printf "Fatal: ELEMENT_KEYSTORE_PATH is not defined in the environment.\n" + envError=1 +fi +# Keystore password +keyStorePassword="${ELEMENT_KEYSTORE_PASSWORD}" +if [[ -z "${keyStorePassword}" ]]; then + printf "Fatal: ELEMENT_KEYSTORE_PASSWORD is not defined in the environment.\n" + envError=1 +fi +# Key password +keyPassword="${ELEMENT_KEY_PASSWORD}" +if [[ -z "${keyPassword}" ]]; then + printf "Fatal: ELEMENT_KEY_PASSWORD is not defined in the environment.\n" + envError=1 +fi +# GitHub token +gitHubToken="${ELEMENT_GITHUB_TOKEN}" +if [[ -z "${gitHubToken}" ]]; then + printf "Fatal: ELEMENT_GITHUB_TOKEN is not defined in the environment.\n" + envError=1 +fi +# Android home +androidHome="${ANDROID_HOME}" +if [[ -z "${androidHome}" ]]; then + printf "Fatal: ANDROID_HOME is not defined in the environment.\n" + envError=1 +fi +# @elementbot:matrix.org matrix token / Not mandatory +elementBotToken="${ELEMENT_BOT_MATRIX_TOKEN}" +if [[ -z "${elementBotToken}" ]]; then + printf "Warning: ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment.\n" fi -releaseScriptFullPath="${releaseScriptLocation}/releaseElement2.sh" - -if [[ ! -f ${releaseScriptFullPath} ]]; then - printf "Fatal: release script not found at ${releaseScriptFullPath}.\n" +if [ ${envError} == 1 ]; then exit 1 fi +buildToolsVersion="30.0.2" +buildToolsPath="${androidHome}/build-tools/${buildToolsVersion}" + +if [[ ! -d ${buildToolsPath} ]]; then + printf "Fatal: ${buildToolsPath} folder not found, ensure that you have installed the SDK version ${buildToolsVersion}.\n" + exit 1 +fi + # Check if git flow is enabled git flow config >/dev/null 2>&1 if [[ $? == 0 ]] then - printf "Git flow is initialized" + printf "Git flow is initialized\n" else printf "Git flow is not initialized. Initializing...\n" # All default value, just set 'v' for tag prefix git flow init -d -t 'v' fi +printf "OK\n" + +printf "\n================================================================================\n" # Guessing version to propose a default version versionMajorCandidate=`grep "ext.versionMajor" ./vector-app/build.gradle | cut -d " " -f3` versionMinorCandidate=`grep "ext.versionMinor" ./vector-app/build.gradle | cut -d " " -f3` versionPatchCandidate=`grep "ext.versionPatch" ./vector-app/build.gradle | cut -d " " -f3` versionCandidate="${versionMajorCandidate}.${versionMinorCandidate}.${versionPatchCandidate}" -printf "\n" read -p "Please enter the release version (example: ${versionCandidate}). Just press enter if ${versionCandidate} is correct. " version version=${version:-${versionCandidate}} @@ -229,14 +267,89 @@ printf "Wait for the GitHub action https://github.com/vector-im/element-android/ read -p "After GHA is finished, please enter the artifact URL (for 'vector-gplay-release-unsigned'): " artifactUrl printf "\n================================================================================\n" -printf "Running the release script...\n" -cd ${releaseScriptLocation} -${releaseScriptFullPath} "v${version}" ${artifactUrl} -cd - +printf "Downloading the artifact...\n" + +# Download files +targetPath="./tmp/Element/${version}" + +# Ignore error +set +e + +python3 ./tools/release/download_github_artifacts.py \ + --token ${gitHubToken} \ + --artifactUrl ${artifactUrl} \ + --directory ${targetPath} \ + --ignoreErrors + +# Do not ignore error +set -e + +printf "\n================================================================================\n" +printf "Unzipping the artifact...\n" + +unzip ${targetPath}/vector-gplay-release-unsigned.zip -d ${targetPath} + +# Flatten folder hierarchy +mv ${targetPath}/gplay/release/* ${targetPath} +rm -rf ${targetPath}/gplay + +printf "\n================================================================================\n" +printf "Signing the APKs...\n" + +cp ${targetPath}/vector-gplay-arm64-v8a-release-unsigned.apk \ + ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk +./tools/release/sign_apk_unsafe.sh \ + ${keyStorePath} \ + ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk \ + ${keyStorePassword} \ + ${keyPassword} + +cp ${targetPath}/vector-gplay-armeabi-v7a-release-unsigned.apk \ + ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk +./tools/release/sign_apk_unsafe.sh \ + ${keyStorePath} \ + ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk \ + ${keyStorePassword} \ + ${keyPassword} + +cp ${targetPath}/vector-gplay-x86-release-unsigned.apk \ + ${targetPath}/vector-gplay-x86-release-signed.apk +./tools/release/sign_apk_unsafe.sh \ + ${keyStorePath} \ + ${targetPath}/vector-gplay-x86-release-signed.apk \ + ${keyStorePassword} \ + ${keyPassword} + +cp ${targetPath}/vector-gplay-x86_64-release-unsigned.apk \ + ${targetPath}/vector-gplay-x86_64-release-signed.apk +./tools/release/sign_apk_unsafe.sh \ + ${keyStorePath} \ + ${targetPath}/vector-gplay-x86_64-release-signed.apk \ + ${keyStorePassword} \ + ${keyPassword} + +# Ref: https://docs.fastlane.tools/getting-started/android/beta-deployment/#uploading-your-app +# set SUPPLY_APK_PATHS="${targetPath}/vector-gplay-arm64-v8a-release-unsigned.apk,${targetPath}/vector-gplay-armeabi-v7a-release-unsigned.apk,${targetPath}/vector-gplay-x86-release-unsigned.apk,${targetPath}/vector-gplay-x86_64-release-unsigned.apk" +# +# ./fastlane beta + +printf "\n================================================================================\n" +printf "Please check the information below:\n" + +printf "File vector-gplay-arm64-v8a-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk | grep package +printf "File vector-gplay-armeabi-v7a-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk | grep package +printf "File vector-gplay-x86-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86-release-signed.apk | grep package +printf "File vector-gplay-x86_64-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86_64-release-signed.apk | grep package + +read -p "\nDoes it look correct? Press enter when it's done." printf "\n================================================================================\n" read -p "Installing apk on a real device, press enter when a real device is connected. " -apkPath="${releaseScriptLocation}/Element/v${version}/vector-gplay-arm64-v8a-release-signed.apk" +apkPath="${targetPath}/vector-gplay-arm64-v8a-release-signed.apk" adb -d install ${apkPath} read -p "Please run the APK on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done." @@ -250,9 +363,8 @@ printf "Message for the Android internal room:\n\n" message="@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!" printf "${message}\n\n" -matrixOrgToken="${MATRIX_ORG_TOKEN}" -if [[ -z "${matrixOrgToken}" ]]; then - read -p "MATRIX_ORG_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter when it's done " +if [[ -z "${elementBotToken}" ]]; then + read -p "ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter when it's done " else read -p "Send this message to the room (yes/no) default to yes? " doSend doSend=${doSend:-yes} @@ -261,7 +373,7 @@ else transactionId=`openssl rand -hex 16` # Element Android internal matrixRoomId="!LiSLXinTDCsepePiYW:matrix.org" - curl -X PUT --data $"{\"msgtype\":\"m.text\",\"body\":\"${message}\"}" -H "Authorization: Bearer ${matrixOrgToken}" https://matrix-client.matrix.org/_matrix/client/r0/rooms/${matrixRoomId}/send/m.room.message/\$local.${transactionId} + curl -X PUT --data $"{\"msgtype\":\"m.text\",\"body\":\"${message}\"}" -H "Authorization: Bearer ${elementBotToken}" https://matrix-client.matrix.org/_matrix/client/r0/rooms/${matrixRoomId}/send/m.room.message/\$local.${transactionId} else printf "Message not sent, please send it manually!\n" fi From 381103383ec983b3232770214946fb3ce0e95671 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 1 Dec 2022 17:44:12 +0100 Subject: [PATCH 039/197] Fix unit tests. --- .../vector/app/features/onboarding/OnboardingViewModelTest.kt | 3 +++ .../im/vector/app/test/fakes/FakeAuthenticationService.kt | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index 718f1ec7a9c..1666491afb5 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -1152,11 +1152,13 @@ class OnboardingViewModelTest { resultingState: SelectedHomeserverState, config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG, fingerprint: Fingerprint? = null, + canLoginWithQrCode: Boolean = false, ) { fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config) fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration) fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString()) + fakeAuthenticationService.givenIsQrLoginSupported(config, canLoginWithQrCode) } private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) { @@ -1164,6 +1166,7 @@ class OnboardingViewModelTest { fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error)) fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString()) + fakeAuthenticationService.givenIsQrLoginSupported(A_HOMESERVER_CONFIG, false) } private fun givenUserNameIsAvailable(userName: String) { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt index af539131693..5d0e317c570 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt @@ -58,6 +58,10 @@ class FakeAuthenticationService : AuthenticationService by mockk() { coEvery { getWellKnownData(matrixId, config) } returns result } + fun givenIsQrLoginSupported(config: HomeServerConnectionConfig, result: Boolean) { + coEvery { isQrLoginSupported(config) } returns result + } + fun givenWellKnownThrows(matrixId: String, config: HomeServerConnectionConfig?, cause: Throwable) { coEvery { getWellKnownData(matrixId, config) } throws cause } From b6aae0c7c10162ba1f502bc4f84b0cc8de196ebb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 1 Dec 2022 17:51:44 +0100 Subject: [PATCH 040/197] Add unit test for canLoginWithQrCode = true --- .../onboarding/OnboardingViewModelTest.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index 1666491afb5..92083eb50b6 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -160,6 +160,28 @@ class OnboardingViewModelTest { .finish() } + @Test + fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver qrCode supported`() = runTest { + val test = viewModel.test() + fakeVectorFeatures.givenCombinedLoginEnabled() + givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE, canLoginWithQrCode = true) + + viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn)) + + test + .assertStatesChanges( + initialState, + { copy(onboardingFlow = OnboardingFlow.SignIn) }, + { copy(isLoading = true) }, + { copy(canLoginWithQrCode = true) }, + { copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE) }, + { copy(signMode = SignMode.SignIn) }, + { copy(isLoading = false) } + ) + .assertEvents(OnboardingViewEvents.OpenCombinedLogin) + .finish() + } + @Test fun `given can successfully login in with token, when logging in with token, then emits AccountSignedIn`() = runTest { val test = viewModel.test() From c12906971a2ee19218127295f78d364f8750f380 Mon Sep 17 00:00:00 2001 From: Jonny Andrew Date: Thu, 1 Dec 2022 17:15:23 +0000 Subject: [PATCH 041/197] Move changelog entry to correct dir --- 7658.bugfix => changelog.d/7658.bugfix | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename 7658.bugfix => changelog.d/7658.bugfix (100%) diff --git a/7658.bugfix b/changelog.d/7658.bugfix similarity index 100% rename from 7658.bugfix rename to changelog.d/7658.bugfix From b70370b21704e5908a9c18438be08cd8eb337930 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Dec 2022 23:02:57 +0000 Subject: [PATCH 042/197] Bump soloader from 0.10.4 to 0.10.5 Bumps [soloader](https://github.com/facebook/soloader) from 0.10.4 to 0.10.5. - [Release notes](https://github.com/facebook/soloader/releases) - [Commits](https://github.com/facebook/soloader/commits) --- updated-dependencies: - dependency-name: com.facebook.soloader:soloader dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- vector-app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 68e20996adb..1096c7ee63b 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -359,7 +359,7 @@ dependencies { debugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - debugImplementation 'com.facebook.soloader:soloader:0.10.4' + debugImplementation 'com.facebook.soloader:soloader:0.10.5' debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0" gplayImplementation "com.google.android.gms:play-services-location:21.0.1" From f0ad75a2b7f0ac152306952c6e466b14a75dc47e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Dec 2022 23:03:43 +0000 Subject: [PATCH 043/197] Bump flipper from 0.175.0 to 0.176.0 Bumps `flipper` from 0.175.0 to 0.176.0. Updates `flipper` from 0.175.0 to 0.176.0 - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.175.0...v0.176.0) Updates `flipper-network-plugin` from 0.175.0 to 0.176.0 - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.175.0...v0.176.0) --- updated-dependencies: - dependency-name: com.facebook.flipper:flipper dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.facebook.flipper:flipper-network-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index cb10170e939..27bca6434ff 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -17,7 +17,7 @@ def markwon = "4.6.2" def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.175.0" +def flipper = "0.176.0" def epoxy = "5.0.0" def mavericks = "3.0.1" def glide = "4.14.2" From 20b1eaba9e89bda737a61faec5cb3340ff5a1282 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 2 Dec 2022 08:41:33 +0000 Subject: [PATCH 044/197] Fix crash in message composer when room is missing (#7683) This error was seen before but has been reintroduced during refactoring. - see https://github.com/vector-im/element-android/pull/6978 --- changelog.d/7683.bugfix | 2 + .../composer/MessageComposerViewModel.kt | 234 +++++++++--------- .../composer/MessageComposerViewState.kt | 5 +- 3 files changed, 127 insertions(+), 114 deletions(-) create mode 100644 changelog.d/7683.bugfix diff --git a/changelog.d/7683.bugfix b/changelog.d/7683.bugfix new file mode 100644 index 00000000000..3922253ba6f --- /dev/null +++ b/changelog.d/7683.bugfix @@ -0,0 +1,2 @@ +Fix crash in message composer when room is missing + diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index a8be2be5e20..c02eb1fa8a6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -59,6 +59,7 @@ import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoomSummary +import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent @@ -89,39 +90,44 @@ class MessageComposerViewModel @AssistedInject constructor( private val voiceBroadcastHelper: VoiceBroadcastHelper, ) : VectorViewModel(initialState) { - private val room = session.getRoom(initialState.roomId)!! + private val room = session.getRoom(initialState.roomId) // Keep it out of state to avoid invalidate being called private var currentComposerText: CharSequence = "" init { - loadDraftIfAny() - observePowerLevelAndEncryption() - observeVoiceBroadcast() - subscribeToStateInternal() + if (room != null) { + loadDraftIfAny(room) + observePowerLevelAndEncryption(room) + observeVoiceBroadcast(room) + subscribeToStateInternal() + } else { + onRoomError() + } } override fun handle(action: MessageComposerAction) { + val room = this.room ?: return when (action) { - is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action) - is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) + is MessageComposerAction.EnterEditMode -> handleEnterEditMode(room, action) + is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(room, action) is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action) - is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(action) - is MessageComposerAction.SendMessage -> handleSendMessage(action) - is MessageComposerAction.UserIsTyping -> handleUserIsTyping(action) + is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(room, action) + is MessageComposerAction.SendMessage -> handleSendMessage(room, action) + is MessageComposerAction.UserIsTyping -> handleUserIsTyping(room, action) is MessageComposerAction.OnTextChanged -> handleOnTextChanged(action) is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action) - is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage() - is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled, action.rootThreadEventId) + is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage(room) + is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(room, action.isCancelled, action.rootThreadEventId) is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action) MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage() MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback() - is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData) - is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText) + is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(room, action.attachmentData) + is MessageComposerAction.OnEntersBackground -> handleEntersBackground(room, action.composerText) is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action) is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action) is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action) - is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action) + is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(room, action) is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action) is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action) } @@ -157,7 +163,7 @@ class MessageComposerViewModel @AssistedInject constructor( copy(sendMode = SendMode.Regular(currentComposerText, action.fromSharing)) } - private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) { + private fun handleEnterEditMode(room: Room, action: MessageComposerAction.EnterEditMode) { room.getTimelineEvent(action.eventId)?.let { timelineEvent -> val formatted = vectorPreferences.isRichTextEditorEnabled() setState { copy(sendMode = SendMode.Edit(timelineEvent, timelineEvent.getTextEditableContent(formatted))) } @@ -168,7 +174,7 @@ class MessageComposerViewModel @AssistedInject constructor( setState { copy(isFullScreen = action.isFullScreen) } } - private fun observePowerLevelAndEncryption() { + private fun observePowerLevelAndEncryption(room: Room) { combine( PowerLevelsFlowFactory(room).createFlow(), room.flow().liveRoomSummary().unwrap() @@ -194,7 +200,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun observeVoiceBroadcast() { + private fun observeVoiceBroadcast(room: Room) { room.stateService().getStateEventLive(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(session.myUserId)) .asFlow() .unwrap() @@ -204,19 +210,19 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) { + private fun handleEnterQuoteMode(room: Room, action: MessageComposerAction.EnterQuoteMode) { room.getTimelineEvent(action.eventId)?.let { timelineEvent -> setState { copy(sendMode = SendMode.Quote(timelineEvent, currentComposerText)) } } } - private fun handleEnterReplyMode(action: MessageComposerAction.EnterReplyMode) { + private fun handleEnterReplyMode(room: Room, action: MessageComposerAction.EnterReplyMode) { room.getTimelineEvent(action.eventId)?.let { timelineEvent -> setState { copy(sendMode = SendMode.Reply(timelineEvent, currentComposerText)) } } } - private fun handleSendMessage(action: MessageComposerAction.SendMessage) { + private fun handleSendMessage(room: Room, action: MessageComposerAction.SendMessage) { withState { state -> analyticsTracker.capture(state.toAnalyticsComposer()).also { setState { copy(startsThread = false) } @@ -246,7 +252,7 @@ class MessageComposerViewModel @AssistedInject constructor( } _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() + popDraft(room) } is ParsedCommand.ErrorSyntax -> { _viewEvents.post(MessageComposerViewEvents.SlashCommandError(parsedCommand.command)) @@ -272,7 +278,7 @@ class MessageComposerViewModel @AssistedInject constructor( room.sendService().sendTextMessage(parsedCommand.message, autoMarkdown = false) } _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() + popDraft(room) } is ParsedCommand.SendFormattedText -> { // Send the text message to the room, without markdown @@ -290,23 +296,23 @@ class MessageComposerViewModel @AssistedInject constructor( ) } _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() + popDraft(room) } is ParsedCommand.ChangeRoomName -> { - handleChangeRoomNameSlashCommand(parsedCommand) + handleChangeRoomNameSlashCommand(room, parsedCommand) } is ParsedCommand.Invite -> { - handleInviteSlashCommand(parsedCommand) + handleInviteSlashCommand(room, parsedCommand) } is ParsedCommand.Invite3Pid -> { - handleInvite3pidSlashCommand(parsedCommand) + handleInvite3pidSlashCommand(room, parsedCommand) } is ParsedCommand.SetUserPowerLevel -> { - handleSetUserPowerLevel(parsedCommand) + handleSetUserPowerLevel(room, parsedCommand) } is ParsedCommand.DevTools -> { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.ClearScalarToken -> { // TODO @@ -315,29 +321,29 @@ class MessageComposerViewModel @AssistedInject constructor( is ParsedCommand.SetMarkdown -> { vectorPreferences.setMarkdownEnabled(parsedCommand.enable) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.BanUser -> { - handleBanSlashCommand(parsedCommand) + handleBanSlashCommand(room, parsedCommand) } is ParsedCommand.UnbanUser -> { - handleUnbanSlashCommand(parsedCommand) + handleUnbanSlashCommand(room, parsedCommand) } is ParsedCommand.IgnoreUser -> { - handleIgnoreSlashCommand(parsedCommand) + handleIgnoreSlashCommand(room, parsedCommand) } is ParsedCommand.UnignoreUser -> { handleUnignoreSlashCommand(parsedCommand) } is ParsedCommand.RemoveUser -> { - handleRemoveSlashCommand(parsedCommand) + handleRemoveSlashCommand(room, parsedCommand) } is ParsedCommand.JoinRoom -> { handleJoinToAnotherRoomSlashCommand(parsedCommand) - popDraft() + popDraft(room) } is ParsedCommand.PartRoom -> { - handlePartSlashCommand(parsedCommand) + handlePartSlashCommand(room, parsedCommand) } is ParsedCommand.SendEmote -> { if (state.rootThreadEventId != null) { @@ -355,7 +361,7 @@ class MessageComposerViewModel @AssistedInject constructor( ) } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.SendRainbow -> { val message = parsedCommand.message.toString() @@ -369,7 +375,7 @@ class MessageComposerViewModel @AssistedInject constructor( room.sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message)) } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.SendRainbowEmote -> { val message = parsedCommand.message.toString() @@ -385,7 +391,7 @@ class MessageComposerViewModel @AssistedInject constructor( } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.SendSpoiler -> { val text = "[${stringProvider.getString(R.string.spoiler)}](${parsedCommand.message})" @@ -403,53 +409,53 @@ class MessageComposerViewModel @AssistedInject constructor( ) } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.SendShrug -> { - sendPrefixedMessage("¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId) + sendPrefixedMessage(room, "¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.SendLenny -> { - sendPrefixedMessage("( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId) + sendPrefixedMessage(room, "( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.SendTableFlip -> { - sendPrefixedMessage("(╯°□°)╯︵ ┻━┻", parsedCommand.message, state.rootThreadEventId) + sendPrefixedMessage(room, "(╯°□°)╯︵ ┻━┻", parsedCommand.message, state.rootThreadEventId) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.SendChatEffect -> { - sendChatEffect(parsedCommand) + sendChatEffect(room, parsedCommand) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.ChangeTopic -> { - handleChangeTopicSlashCommand(parsedCommand) + handleChangeTopicSlashCommand(room, parsedCommand) } is ParsedCommand.ChangeDisplayName -> { - handleChangeDisplayNameSlashCommand(parsedCommand) + handleChangeDisplayNameSlashCommand(room, parsedCommand) } is ParsedCommand.ChangeDisplayNameForRoom -> { - handleChangeDisplayNameForRoomSlashCommand(parsedCommand) + handleChangeDisplayNameForRoomSlashCommand(room, parsedCommand) } is ParsedCommand.ChangeRoomAvatar -> { - handleChangeRoomAvatarSlashCommand(parsedCommand) + handleChangeRoomAvatarSlashCommand(room, parsedCommand) } is ParsedCommand.ChangeAvatarForRoom -> { - handleChangeAvatarForRoomSlashCommand(parsedCommand) + handleChangeAvatarForRoomSlashCommand(room, parsedCommand) } is ParsedCommand.ShowUser -> { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) handleWhoisSlashCommand(parsedCommand) - popDraft() + popDraft(room) } is ParsedCommand.DiscardSession -> { if (room.roomCryptoService().isEncrypted()) { session.cryptoService().discardOutboundSession(room.roomId) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } else { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post( @@ -474,7 +480,7 @@ class MessageComposerViewModel @AssistedInject constructor( null, true ) - popDraft() + popDraft(room) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) @@ -493,7 +499,7 @@ class MessageComposerViewModel @AssistedInject constructor( null, false ) - popDraft() + popDraft(room) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) @@ -506,7 +512,7 @@ class MessageComposerViewModel @AssistedInject constructor( viewModelScope.launch(Dispatchers.IO) { try { session.spaceService().joinSpace(parsedCommand.spaceIdOrAlias) - popDraft() + popDraft(room) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) @@ -518,7 +524,7 @@ class MessageComposerViewModel @AssistedInject constructor( viewModelScope.launch(Dispatchers.IO) { try { session.roomService().leaveRoom(parsedCommand.roomId) - popDraft() + popDraft(room) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) @@ -534,7 +540,7 @@ class MessageComposerViewModel @AssistedInject constructor( ) ) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } } } @@ -583,7 +589,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() + popDraft(room) } is SendMode.Quote -> { room.sendService().sendQuotedTextMessage( @@ -594,7 +600,7 @@ class MessageComposerViewModel @AssistedInject constructor( rootThreadEventId = state.rootThreadEventId ) _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() + popDraft(room) } is SendMode.Reply -> { val timelineEvent = state.sendMode.timelineEvent @@ -619,7 +625,7 @@ class MessageComposerViewModel @AssistedInject constructor( ) _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() + popDraft(room) } is SendMode.Voice -> { // do nothing @@ -628,10 +634,10 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun popDraft() = withState { + private fun popDraft(room: Room) = withState { if (it.sendMode is SendMode.Regular && it.sendMode.fromSharing) { // If we were sharing, we want to get back our last value from draft - loadDraftIfAny() + loadDraftIfAny(room) } else { // Otherwise we clear the composer and remove the draft from db setState { copy(sendMode = SendMode.Regular("", false)) } @@ -641,7 +647,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun loadDraftIfAny() { + private fun loadDraftIfAny(room: Room) { val currentDraft = room.draftService().getDraft() setState { copy( @@ -670,7 +676,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleUserIsTyping(action: MessageComposerAction.UserIsTyping) { + private fun handleUserIsTyping(room: Room, action: MessageComposerAction.UserIsTyping) { if (vectorPreferences.sendTypingNotifs()) { if (action.isTyping) { room.typingService().userIsTyping() @@ -680,7 +686,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) { + private fun sendChatEffect(room: Room, sendChatEffect: ParsedCommand.SendChatEffect) { // If message is blank, convert to an emote, with default message if (sendChatEffect.message.isBlank()) { val defaultMessage = stringProvider.getString( @@ -732,25 +738,25 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { - launchSlashCommandFlowSuspendable(changeTopic) { + private fun handleChangeTopicSlashCommand(room: Room, changeTopic: ParsedCommand.ChangeTopic) { + launchSlashCommandFlowSuspendable(room, changeTopic) { room.stateService().updateTopic(changeTopic.topic) } } - private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { - launchSlashCommandFlowSuspendable(invite) { + private fun handleInviteSlashCommand(room: Room, invite: ParsedCommand.Invite) { + launchSlashCommandFlowSuspendable(room, invite) { room.membershipService().invite(invite.userId, invite.reason) } } - private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) { - launchSlashCommandFlowSuspendable(invite) { + private fun handleInvite3pidSlashCommand(room: Room, invite: ParsedCommand.Invite3Pid) { + launchSlashCommandFlowSuspendable(room, invite) { room.membershipService().invite3pid(invite.threePid) } } - private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) { + private fun handleSetUserPowerLevel(room: Room, setUserPowerLevel: ParsedCommand.SetUserPowerLevel) { val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) ?.content ?.toModel() @@ -758,19 +764,19 @@ class MessageComposerViewModel @AssistedInject constructor( ?.toContent() ?: return - launchSlashCommandFlowSuspendable(setUserPowerLevel) { + launchSlashCommandFlowSuspendable(room, setUserPowerLevel) { room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent) } } - private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) { - launchSlashCommandFlowSuspendable(changeDisplayName) { + private fun handleChangeDisplayNameSlashCommand(room: Room, changeDisplayName: ParsedCommand.ChangeDisplayName) { + launchSlashCommandFlowSuspendable(room, changeDisplayName) { session.profileService().setDisplayName(session.myUserId, changeDisplayName.displayName) } } - private fun handlePartSlashCommand(command: ParsedCommand.PartRoom) { - launchSlashCommandFlowSuspendable(command) { + private fun handlePartSlashCommand(room: Room, command: ParsedCommand.PartRoom) { + launchSlashCommandFlowSuspendable(room, command) { if (command.roomAlias == null) { // Leave the current room room @@ -785,39 +791,39 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleRemoveSlashCommand(removeUser: ParsedCommand.RemoveUser) { - launchSlashCommandFlowSuspendable(removeUser) { + private fun handleRemoveSlashCommand(room: Room, removeUser: ParsedCommand.RemoveUser) { + launchSlashCommandFlowSuspendable(room, removeUser) { room.membershipService().remove(removeUser.userId, removeUser.reason) } } - private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) { - launchSlashCommandFlowSuspendable(ban) { + private fun handleBanSlashCommand(room: Room, ban: ParsedCommand.BanUser) { + launchSlashCommandFlowSuspendable(room, ban) { room.membershipService().ban(ban.userId, ban.reason) } } - private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) { - launchSlashCommandFlowSuspendable(unban) { + private fun handleUnbanSlashCommand(room: Room, unban: ParsedCommand.UnbanUser) { + launchSlashCommandFlowSuspendable(room, unban) { room.membershipService().unban(unban.userId, unban.reason) } } - private fun handleChangeRoomNameSlashCommand(changeRoomName: ParsedCommand.ChangeRoomName) { - launchSlashCommandFlowSuspendable(changeRoomName) { + private fun handleChangeRoomNameSlashCommand(room: Room, changeRoomName: ParsedCommand.ChangeRoomName) { + launchSlashCommandFlowSuspendable(room, changeRoomName) { room.stateService().updateName(changeRoomName.name) } } - private fun getMyRoomMemberContent(): RoomMemberContent? { + private fun getMyRoomMemberContent(room: Room): RoomMemberContent? { return room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId)) ?.content ?.toModel() } - private fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) { - launchSlashCommandFlowSuspendable(changeDisplayName) { - getMyRoomMemberContent() + private fun handleChangeDisplayNameForRoomSlashCommand(room: Room, changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) { + launchSlashCommandFlowSuspendable(room, changeDisplayName) { + getMyRoomMemberContent(room) ?.copy(displayName = changeDisplayName.displayName) ?.toContent() ?.let { @@ -826,15 +832,15 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleChangeRoomAvatarSlashCommand(changeAvatar: ParsedCommand.ChangeRoomAvatar) { - launchSlashCommandFlowSuspendable(changeAvatar) { + private fun handleChangeRoomAvatarSlashCommand(room: Room, changeAvatar: ParsedCommand.ChangeRoomAvatar) { + launchSlashCommandFlowSuspendable(room, changeAvatar) { room.stateService().sendStateEvent(EventType.STATE_ROOM_AVATAR, stateKey = "", RoomAvatarContent(changeAvatar.url).toContent()) } } - private fun handleChangeAvatarForRoomSlashCommand(changeAvatar: ParsedCommand.ChangeAvatarForRoom) { - launchSlashCommandFlowSuspendable(changeAvatar) { - getMyRoomMemberContent() + private fun handleChangeAvatarForRoomSlashCommand(room: Room, changeAvatar: ParsedCommand.ChangeAvatarForRoom) { + launchSlashCommandFlowSuspendable(room, changeAvatar) { + getMyRoomMemberContent(room) ?.copy(avatarUrl = changeAvatar.url) ?.toContent() ?.let { @@ -843,8 +849,8 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleIgnoreSlashCommand(ignore: ParsedCommand.IgnoreUser) { - launchSlashCommandFlowSuspendable(ignore) { + private fun handleIgnoreSlashCommand(room: Room, ignore: ParsedCommand.IgnoreUser) { + launchSlashCommandFlowSuspendable(room, ignore) { session.userService().ignoreUserIds(listOf(ignore.userId)) } } @@ -853,15 +859,15 @@ class MessageComposerViewModel @AssistedInject constructor( _viewEvents.post(MessageComposerViewEvents.SlashCommandConfirmationRequest(unignore)) } - private fun handleSlashCommandConfirmed(action: MessageComposerAction.SlashCommandConfirmed) { + private fun handleSlashCommandConfirmed(room: Room, action: MessageComposerAction.SlashCommandConfirmed) { when (action.parsedCommand) { - is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(action.parsedCommand) + is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(room, action.parsedCommand) else -> TODO("Not handled yet") } } - private fun handleUnignoreSlashCommandConfirmed(unignore: ParsedCommand.UnignoreUser) { - launchSlashCommandFlowSuspendable(unignore) { + private fun handleUnignoreSlashCommandConfirmed(room: Room, unignore: ParsedCommand.UnignoreUser) { + launchSlashCommandFlowSuspendable(room, unignore) { session.userService().unIgnoreUserIds(listOf(unignore.userId)) } } @@ -870,7 +876,7 @@ class MessageComposerViewModel @AssistedInject constructor( _viewEvents.post(MessageComposerViewEvents.OpenRoomMemberProfile(whois.userId)) } - private fun sendPrefixedMessage(prefix: String, message: CharSequence, rootThreadEventId: String?) { + private fun sendPrefixedMessage(room: Room, prefix: String, message: CharSequence, rootThreadEventId: String?) { val sequence = buildString { append(prefix) if (message.isNotEmpty()) { @@ -886,7 +892,7 @@ class MessageComposerViewModel @AssistedInject constructor( /** * Convert a send mode to a draft and save the draft. */ - private fun handleSaveTextDraft(draft: String) = withState { + private fun handleSaveTextDraft(room: Room, draft: String) = withState { session.coroutineScope.launch { when { it.sendMode is SendMode.Regular && !it.sendMode.fromSharing -> { @@ -909,7 +915,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleStartRecordingVoiceMessage() { + private fun handleStartRecordingVoiceMessage(room: Room) { try { audioMessageHelper.startRecording(room.roomId) } catch (failure: Throwable) { @@ -917,7 +923,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) { + private fun handleEndRecordingVoiceMessage(room: Room, isCancelled: Boolean, rootThreadEventId: String? = null) { audioMessageHelper.stopPlayback() if (isCancelled) { audioMessageHelper.deleteRecording() @@ -964,7 +970,7 @@ class MessageComposerViewModel @AssistedInject constructor( audioMessageHelper.stopAllVoiceActions(deleteRecord) } - private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) { + private fun handleInitializeVoiceRecorder(room: Room, attachmentData: ContentAttachmentData) { audioMessageHelper.initializeRecorder(room.roomId, attachmentData) setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) } } @@ -985,7 +991,7 @@ class MessageComposerViewModel @AssistedInject constructor( audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration) } - private fun handleEntersBackground(composerText: String) { + private fun handleEntersBackground(room: Room, composerText: String) { // Always stop all voice actions. It may be playing in timeline or active recording val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false) // TODO remove this when there will be a listening indicator outside of the timeline @@ -1001,7 +1007,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } } else { - handleSaveTextDraft(draft = composerText) + handleSaveTextDraft(room = room, draft = composerText) } } @@ -1009,12 +1015,12 @@ class MessageComposerViewModel @AssistedInject constructor( _viewEvents.post(MessageComposerViewEvents.InsertUserDisplayName(action.userId)) } - private fun launchSlashCommandFlowSuspendable(parsedCommand: ParsedCommand, block: suspend () -> Unit) { + private fun launchSlashCommandFlowSuspendable(room: Room, parsedCommand: ParsedCommand, block: suspend () -> Unit) { _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading) viewModelScope.launch { val event = try { block() - popDraft() + popDraft(room) MessageComposerViewEvents.SlashCommandResultOk(parsedCommand) } catch (failure: Throwable) { MessageComposerViewEvents.SlashCommandResultError(failure) @@ -1023,6 +1029,10 @@ class MessageComposerViewModel @AssistedInject constructor( } } + private fun onRoomError() = setState { + copy(isRoomError = true) + } + @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { override fun create(initialState: MessageComposerViewState): MessageComposerViewModel diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt index bf40c189953..235ca574fcc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt @@ -62,6 +62,7 @@ fun CanSendStatus.boolean(): Boolean { data class MessageComposerViewState( val roomId: String, + val isRoomError: Boolean = false, val canSendMessage: CanSendStatus = CanSendStatus.Allowed, val isSendButtonVisible: Boolean = false, val rootThreadEventId: String? = null, @@ -88,8 +89,8 @@ data class MessageComposerViewState( val isVoiceMessageIdle = !isVoiceRecording - val isComposerVisible = canSendMessage.boolean() && !isVoiceRecording - val isVoiceMessageRecorderVisible = canSendMessage.boolean() && !isSendButtonVisible + val isComposerVisible = canSendMessage.boolean() && !isVoiceRecording && !isRoomError + val isVoiceMessageRecorderVisible = canSendMessage.boolean() && !isSendButtonVisible && !isRoomError constructor(args: TimelineArgs) : this( roomId = args.roomId, From 310ea99c443ef01083241bfc542ee34c7b1674a0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 2 Dec 2022 10:50:08 +0100 Subject: [PATCH 045/197] Fix bad pills color background. For light and dark theme the color is now 61708B (iso EleWeb) --- changelog.d/7274.bugfix | 1 + library/ui-styles/src/main/res/values/palette.xml | 2 +- library/ui-styles/src/main/res/values/theme_dark.xml | 2 +- library/ui-styles/src/main/res/values/theme_light.xml | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/7274.bugfix diff --git a/changelog.d/7274.bugfix b/changelog.d/7274.bugfix new file mode 100644 index 00000000000..e99daceb897 --- /dev/null +++ b/changelog.d/7274.bugfix @@ -0,0 +1 @@ +Fix bad pills color background. For light and dark theme the color is now 61708B (iso EleWeb) diff --git a/library/ui-styles/src/main/res/values/palette.xml b/library/ui-styles/src/main/res/values/palette.xml index 73ac768919f..999dccf167b 100644 --- a/library/ui-styles/src/main/res/values/palette.xml +++ b/library/ui-styles/src/main/res/values/palette.xml @@ -44,4 +44,4 @@ #15191E #21262C - \ No newline at end of file + diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml index d5aaa88ab81..9665b7335cf 100644 --- a/library/ui-styles/src/main/res/values/theme_dark.xml +++ b/library/ui-styles/src/main/res/values/theme_dark.xml @@ -53,7 +53,7 @@ ?vctr_content_quinary ?vctr_system ?vctr_system - ?vctr_content_tertiary + ?vctr_notice_secondary @color/element_accent_dark diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml index 1978db9139b..c19fe8a1119 100644 --- a/library/ui-styles/src/main/res/values/theme_light.xml +++ b/library/ui-styles/src/main/res/values/theme_light.xml @@ -53,7 +53,7 @@ ?vctr_content_quinary ?vctr_system ?vctr_system - ?vctr_content_tertiary + ?vctr_notice_secondary @color/element_accent_light From 4050975a19045d2c1152f7ee5e4dde6d27c3431f Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 2 Dec 2022 18:15:10 +0300 Subject: [PATCH 046/197] Implement new logic for new login banner. --- .../debug/features/DebugVectorFeatures.kt | 4 ++ .../im/vector/app/features/VectorFeatures.kt | 2 +- .../app/features/home/HomeDetailFragment.kt | 4 +- .../home/IsNewLoginAlertShownUseCase.kt | 31 ++++++++++++++ .../features/home/NewHomeDetailFragment.kt | 4 +- .../home/SetNewLoginAlertShownUseCase.kt | 31 ++++++++++++++ .../SetUnverifiedSessionsAlertShownUseCase.kt | 34 +++++++++++++++ ...houldShowUnverifiedSessionsAlertUseCase.kt | 6 ++- .../UnknownDeviceDetectorSharedViewModel.kt | 41 ++++++------------- .../features/settings/VectorPreferences.kt | 35 ++++++++-------- ...dShowUnverifiedSessionsAlertUseCaseTest.kt | 11 ++--- .../app/test/fakes/FakeVectorPreferences.kt | 2 +- 12 files changed, 145 insertions(+), 60 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/IsNewLoginAlertShownUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/SetNewLoginAlertShownUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index 5c497c24ec6..2134c8cf2c5 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -88,6 +88,9 @@ class DebugVectorFeatures( override fun isVoiceBroadcastEnabled(): Boolean = read(DebugFeatureKeys.voiceBroadcastEnabled) ?: vectorFeatures.isVoiceBroadcastEnabled() + override fun isUnverifiedSessionsAlertEnabled(): Boolean = read(DebugFeatureKeys.unverifiedSessionsAlertEnabled) + ?: vectorFeatures.isUnverifiedSessionsAlertEnabled() + fun override(value: T?, key: Preferences.Key) = updatePreferences { if (value == null) { it.remove(key) @@ -151,4 +154,5 @@ object DebugFeatureKeys { val qrCodeLoginForAllServers = booleanPreferencesKey("qr-code-login-for-all-servers") val reciprocateQrCodeLogin = booleanPreferencesKey("reciprocate-qr-code-login") val voiceBroadcastEnabled = booleanPreferencesKey("voice-broadcast-enabled") + val unverifiedSessionsAlertEnabled = booleanPreferencesKey("unverified-sessions-alert-enabled") } 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 28c2e379265..99abc15f81f 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -64,5 +64,5 @@ class DefaultVectorFeatures : VectorFeatures { override fun isQrCodeLoginForAllServers(): Boolean = false override fun isReciprocateQrCodeLogin(): Boolean = false override fun isVoiceBroadcastEnabled(): Boolean = true - override fun isUnverifiedSessionsAlertEnabled(): Boolean = false + override fun isUnverifiedSessionsAlertEnabled(): Boolean = true } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 7552b934e41..69abeed424f 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -239,12 +239,12 @@ class HomeDetailFragment : .requestSessionVerification(vectorBaseActivity, newest.deviceId ?: "") } unknownDeviceDetectorSharedViewModel.handle( - UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) + UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty()) ) } dismissedAction = Runnable { unknownDeviceDetectorSharedViewModel.handle( - UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) + UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty()) ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/IsNewLoginAlertShownUseCase.kt b/vector/src/main/java/im/vector/app/features/home/IsNewLoginAlertShownUseCase.kt new file mode 100644 index 00000000000..5a0d4743dcb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/IsNewLoginAlertShownUseCase.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home + +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +class IsNewLoginAlertShownUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, +) { + + fun execute(deviceId: String?): Boolean { + deviceId ?: return false + + return vectorPreferences.isNewLoginAlertShownForDevice(deviceId) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt index 62d7e58bdb8..ccd5a7e84bb 100644 --- a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt @@ -253,12 +253,12 @@ class NewHomeDetailFragment : .requestSessionVerification(vectorBaseActivity, newest.deviceId ?: "") } unknownDeviceDetectorSharedViewModel.handle( - UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) + UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty()) ) } dismissedAction = Runnable { unknownDeviceDetectorSharedViewModel.handle( - UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) + UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty()) ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/SetNewLoginAlertShownUseCase.kt b/vector/src/main/java/im/vector/app/features/home/SetNewLoginAlertShownUseCase.kt new file mode 100644 index 00000000000..d313f930438 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/SetNewLoginAlertShownUseCase.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home + +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +class SetNewLoginAlertShownUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, +) { + + fun execute(deviceIds: List) { + deviceIds.forEach { + vectorPreferences.setNewLoginAlertShownForDevice(it) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt b/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt new file mode 100644 index 00000000000..4580ac0f312 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home + +import im.vector.app.core.time.Clock +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +class SetUnverifiedSessionsAlertShownUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, + private val clock: Clock, +) { + + fun execute(deviceIds: List) { + val epochMillis = clock.epochMillis() + deviceIds.forEach { + vectorPreferences.setUnverifiedSessionsAlertLastShownMillis(it, epochMillis) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt b/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt index 0455b4399a1..18c7ed96893 100644 --- a/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt @@ -28,9 +28,11 @@ class ShouldShowUnverifiedSessionsAlertUseCase @Inject constructor( private val clock: Clock, ) { - fun execute(): Boolean { + fun execute(deviceId: String?): Boolean { + deviceId ?: return false + val isUnverifiedSessionsAlertEnabled = vectorFeatures.isUnverifiedSessionsAlertEnabled() - val unverifiedSessionsAlertLastShownMillis = vectorPreferences.getUnverifiedSessionsAlertLastShownMillis() + val unverifiedSessionsAlertLastShownMillis = vectorPreferences.getUnverifiedSessionsAlertLastShownMillis(deviceId) return isUnverifiedSessionsAlertEnabled && clock.epochMillis() - unverifiedSessionsAlertLastShownMillis >= Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS } diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt index 855c47f4bbe..21c7bd6ea1d 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -19,7 +19,6 @@ package im.vector.app.features.home import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksViewModelFactory -import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted @@ -33,7 +32,6 @@ import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.time.Clock -import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn @@ -63,12 +61,16 @@ data class DeviceDetectionInfo( class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( @Assisted initialState: UnknownDevicesState, session: Session, - private val vectorPreferences: VectorPreferences, clock: Clock, + private val shouldShowUnverifiedSessionsAlertUseCase: ShouldShowUnverifiedSessionsAlertUseCase, + private val setUnverifiedSessionsAlertShownUseCase: SetUnverifiedSessionsAlertShownUseCase, + private val isNewLoginAlertShownUseCase: IsNewLoginAlertShownUseCase, + private val setNewLoginAlertShownUseCase: SetNewLoginAlertShownUseCase, ) : VectorViewModel(initialState) { sealed class Action : VectorViewModelAction { data class IgnoreDevice(val deviceIds: List) : Action() + data class IgnoreNewLogin(val deviceIds: List) : Action() } @AssistedFactory @@ -86,8 +88,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( } } - private val ignoredDeviceList = ArrayList() - init { val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId) .firstOrNull { it.deviceId == session.sessionParams.deviceId } @@ -95,12 +95,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( ?: clock.epochMillis() Timber.v("## Detector - Current Session first time seen $currentSessionTs") - ignoredDeviceList.addAll( - vectorPreferences.getUnknownDeviceDismissedList().also { - Timber.v("## Detector - Remembered ignored list $it") - } - ) - combine( session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveMyDevicesInfo(), @@ -114,13 +108,15 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse() } // filter out ignored devices - .filter { !ignoredDeviceList.contains(it.deviceId) } + .filter { shouldShowUnverifiedSessionsAlertUseCase.execute(it.deviceId) } .sortedByDescending { it.lastSeenTs } .map { deviceInfo -> val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firstTimeSeenLocalTs ?: 0 + val isNew = isNewLoginAlertShownUseCase.execute(deviceInfo.deviceId).not() && deviceKnownSince > currentSessionTs + DeviceDetectionInfo( deviceInfo, - deviceKnownSince > currentSessionTs + 60_000, // short window to avoid false positive, + isNew, pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change ) } @@ -150,22 +146,11 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( override fun handle(action: Action) { when (action) { is Action.IgnoreDevice -> { - ignoredDeviceList.addAll(action.deviceIds) - // local echo - withState { state -> - state.unknownSessions.invoke()?.let { detectedSessions -> - val updated = detectedSessions.filter { !action.deviceIds.contains(it.deviceInfo.deviceId) } - setState { - copy(unknownSessions = Success(updated)) - } - } - } + setUnverifiedSessionsAlertShownUseCase.execute(action.deviceIds) + } + is Action.IgnoreNewLogin -> { + setNewLoginAlertShownUseCase.execute(action.deviceIds) } } } - - override fun onCleared() { - vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList) - super.onCleared() - } } 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 3f350800572..e1457b0ebfa 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 @@ -227,8 +227,6 @@ class VectorPreferences @Inject constructor( private const val MEDIA_SAVING_1_MONTH = 2 private const val MEDIA_SAVING_FOREVER = 3 - private const val SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST = "SETTINGS_UNKNWON_DEVICE_DISMISSED_LIST" - private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE" private const val SETTINGS_LABS_ENABLE_LIVE_LOCATION = "SETTINGS_LABS_ENABLE_LIVE_LOCATION" @@ -245,7 +243,8 @@ class VectorPreferences @Inject constructor( // This key will be used to enable user for displaying live user info or not. const val SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO = "SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO" - const val SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS = "SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS" + const val SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS = "SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS_" + const val SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE = "SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE_" // Possible values for TAKE_PHOTO_VIDEO_MODE const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0 @@ -522,18 +521,6 @@ class VectorPreferences @Inject constructor( return defaultPrefs.getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true) } - fun storeUnknownDeviceDismissedList(deviceIds: List) { - defaultPrefs.edit(true) { - putStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, deviceIds.toSet()) - } - } - - fun getUnknownDeviceDismissedList(): List { - return tryOrNull { - defaultPrefs.getStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, null)?.toList() - }.orEmpty() - } - /** * Update the notification ringtone. * @@ -1244,13 +1231,23 @@ class VectorPreferences @Inject constructor( } } - fun getUnverifiedSessionsAlertLastShownMillis(): Long { - return defaultPrefs.getLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS, 0) + fun getUnverifiedSessionsAlertLastShownMillis(deviceId: String): Long { + return defaultPrefs.getLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS + deviceId, 0) + } + + fun setUnverifiedSessionsAlertLastShownMillis(deviceId: String, lastShownMillis: Long) { + defaultPrefs.edit { + putLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS + deviceId, lastShownMillis) + } + } + + fun isNewLoginAlertShownForDevice(deviceId: String): Boolean { + return defaultPrefs.getBoolean(SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE + deviceId, false) } - fun setUnverifiedSessionsAlertLastShownMillis(lastShownMillis: Long) { + fun setNewLoginAlertShownForDevice(deviceId: String) { defaultPrefs.edit { - putLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS, lastShownMillis) + putBoolean(SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE + deviceId, true) } } } diff --git a/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt index cb4b8b2a1f7..5d08499e329 100644 --- a/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt @@ -23,7 +23,8 @@ import im.vector.app.test.fakes.FakeVectorPreferences import org.amshove.kluent.shouldBe import org.junit.Test -private val AN_EPOCH = Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS +private val AN_EPOCH = Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS.toLong() +private const val A_DEVICE_ID = "A_DEVICE_ID" class ShouldShowUnverifiedSessionsAlertUseCaseTest { @@ -42,7 +43,7 @@ class ShouldShowUnverifiedSessionsAlertUseCaseTest { fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(false) fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(0L) - shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe false + shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe false } @Test @@ -51,7 +52,7 @@ class ShouldShowUnverifiedSessionsAlertUseCaseTest { fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(0L) fakeClock.givenEpoch(AN_EPOCH + 1) - shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe true + shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe true } @Test @@ -60,7 +61,7 @@ class ShouldShowUnverifiedSessionsAlertUseCaseTest { fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(AN_EPOCH) fakeClock.givenEpoch(AN_EPOCH * 2 + 1) - shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe true + shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe true } @Test @@ -69,6 +70,6 @@ class ShouldShowUnverifiedSessionsAlertUseCaseTest { fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(AN_EPOCH) fakeClock.givenEpoch(AN_EPOCH + 1) - shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe false + shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe false } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index 101657b260b..77df3ffc7ac 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -58,6 +58,6 @@ class FakeVectorPreferences { } fun givenUnverifiedSessionsAlertLastShownMillis(lastShownMillis: Long) { - every { instance.getUnverifiedSessionsAlertLastShownMillis() } returns lastShownMillis + every { instance.getUnverifiedSessionsAlertLastShownMillis(any()) } returns lastShownMillis } } From 6f934e2d49e77f949b0e1c0eaae3651fabaac88b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 2 Dec 2022 16:50:45 +0100 Subject: [PATCH 047/197] Extract paparazzi rule creation --- .../PaparazziExampleScreenshotTest.kt | 16 +-------- .../im/vector/app/screenshot/PaparazziRule.kt | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt diff --git a/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt b/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt index 65f89dcc6a4..58658651cf7 100644 --- a/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt +++ b/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt @@ -16,14 +16,9 @@ package im.vector.app.screenshot -import android.os.Build import android.widget.ImageView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout -import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_3 -import app.cash.paparazzi.Paparazzi -import app.cash.paparazzi.androidHome -import app.cash.paparazzi.detectEnvironment import im.vector.app.R import org.junit.Rule import org.junit.Test @@ -31,16 +26,7 @@ import org.junit.Test class PaparazziExampleScreenshotTest { @get:Rule - val paparazzi = Paparazzi( - // Apply trick from https://github.com/cashapp/paparazzi/issues/489#issuecomment-1195674603 - environment = detectEnvironment().copy( - platformDir = "${androidHome()}/platforms/android-32", - compileSdkVersion = Build.VERSION_CODES.S_V2 /* 32 */ - ), - deviceConfig = PIXEL_3, - theme = "Theme.Vector.Light", - maxPercentDifference = 0.0, - ) + val paparazzi = createPaparazziRule() @Test fun `example paparazzi test`() { diff --git a/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt b/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt new file mode 100644 index 00000000000..970bb15a258 --- /dev/null +++ b/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt @@ -0,0 +1,35 @@ +/* + * 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.screenshot + +import android.os.Build +import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_3 +import app.cash.paparazzi.Paparazzi +import app.cash.paparazzi.androidHome +import app.cash.paparazzi.detectEnvironment + +fun createPaparazziRule() = Paparazzi( + // Apply trick from https://github.com/cashapp/paparazzi/issues/489#issuecomment-1195674603 + environment = detectEnvironment().copy( + platformDir = "${androidHome()}/platforms/android-32", + compileSdkVersion = Build.VERSION_CODES.S_V2 /* 32 */ + ), + deviceConfig = PIXEL_3, + theme = "Theme.Vector.Light", + maxPercentDifference = 0.0, +) + From e857407bc1dd444bbe77fa7ff7e6399171a58cd3 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 2 Dec 2022 16:50:46 +0100 Subject: [PATCH 048/197] Adding changelog entry --- changelog.d/7693.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7693.feature diff --git a/changelog.d/7693.feature b/changelog.d/7693.feature new file mode 100644 index 00000000000..271964db82f --- /dev/null +++ b/changelog.d/7693.feature @@ -0,0 +1 @@ +[Session manager] Add action to signout all the other session From f576f833391edc677b4526e88059e5d3ba62eef7 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 2 Dec 2022 19:02:55 +0300 Subject: [PATCH 049/197] Add changelog. --- changelog.d/7694.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7694.feature diff --git a/changelog.d/7694.feature b/changelog.d/7694.feature new file mode 100644 index 00000000000..408925974e0 --- /dev/null +++ b/changelog.d/7694.feature @@ -0,0 +1 @@ +Remind unverified sessions with a banner once a week From ab43f4cf14fdfb0781d9afb3d3651b93605208d3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 2 Dec 2022 17:02:45 +0100 Subject: [PATCH 050/197] Add snapshot test for room item --- .../app/screenshot/RoomItemScreenshotTest.kt | 66 +++++++++++++++++++ ..._RoomItemScreenshotTest_item room test.png | 3 + ..._item room two line and highlight test.png | 3 + 3 files changed, 72 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt create mode 100644 vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png create mode 100644 vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png diff --git a/vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt b/vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt new file mode 100644 index 00000000000..d1f4034f435 --- /dev/null +++ b/vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.screenshot + +import android.view.View +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import im.vector.app.R +import im.vector.app.features.home.room.list.UnreadCounterBadgeView +import org.junit.Rule +import org.junit.Test + +class RoomItemScreenshotTest { + + @get:Rule + val paparazzi = createPaparazziRule() + + @Test + fun `item room test`() { + val view = paparazzi.inflate(R.layout.item_room) + + view.findViewById(R.id.roomUnreadIndicator).isVisible = true + view.findViewById(R.id.roomNameView).text = "Room name" + view.findViewById(R.id.roomLastEventTimeView).text = "12:34" + view.findViewById(R.id.subtitleView).text = "Latest message" + view.findViewById(R.id.roomDraftBadge).isVisible = true + view.findViewById(R.id.roomUnreadCounterBadgeView).let { + it.isVisible = true + it.render(UnreadCounterBadgeView.State.Count(8, false)) + } + + paparazzi.snapshot(view) + } + + @Test + fun `item room two line and highlight test`() { + val view = paparazzi.inflate(R.layout.item_room) + + view.findViewById(R.id.roomUnreadIndicator).isVisible = true + view.findViewById(R.id.roomNameView).text = "Room name" + view.findViewById(R.id.roomLastEventTimeView).text = "23:59" + view.findViewById(R.id.subtitleView).text = "Latest message\nOn two lines" + view.findViewById(R.id.roomDraftBadge).isVisible = true + view.findViewById(R.id.roomUnreadCounterBadgeView).let { + it.isVisible = true + it.render(UnreadCounterBadgeView.State.Count(88, true)) + } + + paparazzi.snapshot(view) + } +} diff --git a/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png new file mode 100644 index 00000000000..1e87449b3c0 --- /dev/null +++ b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d33e82c6647bab9dcb3745d8c5a5448d60049279c365b9f64816eb9c958360d2 +size 15015 diff --git a/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png new file mode 100644 index 00000000000..83fcb8d000e --- /dev/null +++ b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91a106e2a3f7310ac05425a2413ccec0aaa07720609d77a2ecd9a9d0d602b296 +size 17232 From 62e2f06e2a4f73bf6de19062f979481979cdc95b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 2 Dec 2022 17:08:29 +0100 Subject: [PATCH 051/197] Adding menu for current session header --- library/ui-strings/src/main/res/values/strings.xml | 1 + .../devices/v2/VectorSettingsDevicesFragment.kt | 11 +++++++---- .../main/res/layout/fragment_settings_devices.xml | 1 + .../main/res/menu/menu_current_session_header.xml | 12 ++++++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 vector/src/main/res/menu/menu_current_session_header.xml diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 58fc62b3470..684bc6f7b23 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3359,6 +3359,7 @@ Sign out of %1$d session Sign out of %1$d sessions + Sign out of all other sessions Show IP address Hide IP address Sign out of this session diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index b27d8a72708..c3715813951 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -247,7 +247,7 @@ class VectorSettingsDevicesFragment : val otherDevices = devices?.filter { it.deviceInfo.deviceId != currentDeviceId } renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount, isCurrentSessionVerified) - renderCurrentDevice(currentDeviceInfo) + renderCurrentSessionView(currentDeviceInfo) renderOtherSessionsView(otherDevices, state.isShowingIpAddress) } else { hideSecurityRecommendations() @@ -310,11 +310,11 @@ class VectorSettingsDevicesFragment : hideOtherSessionsView() } else { views.deviceListHeaderOtherSessions.isVisible = true - val color = colorProvider.getColorFromAttribute(R.attr.colorError) + val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError) val multiSignoutItem = views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout) val nbDevices = otherDevices.size multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices) - multiSignoutItem.setTextColor(color) + multiSignoutItem.setTextColor(colorDestructive) views.deviceListOtherSessions.isVisible = true val devices = if (isShowingIpAddress) otherDevices else otherDevices.map { it.copy(deviceInfo = it.deviceInfo.copy(lastSeenIp = null)) } views.deviceListOtherSessions.render( @@ -335,9 +335,12 @@ class VectorSettingsDevicesFragment : views.deviceListOtherSessions.isVisible = false } - private fun renderCurrentDevice(currentDeviceInfo: DeviceFullInfo?) { + private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?) { currentDeviceInfo?.let { views.deviceListHeaderCurrentSession.isVisible = true + val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError) + val signoutOtherSessionsItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignoutOtherSessions) + signoutOtherSessionsItem.setTextColor(colorDestructive) views.deviceListCurrentSession.isVisible = true val viewState = SessionInfoViewState( isCurrentSession = true, diff --git a/vector/src/main/res/layout/fragment_settings_devices.xml b/vector/src/main/res/layout/fragment_settings_devices.xml index 81347748874..731049f3a2a 100644 --- a/vector/src/main/res/layout/fragment_settings_devices.xml +++ b/vector/src/main/res/layout/fragment_settings_devices.xml @@ -67,6 +67,7 @@ app:layout_constraintTop_toBottomOf="@id/deviceListSecurityRecommendationsDivider" app:sessionsListHeaderDescription="" app:sessionsListHeaderHasLearnMoreLink="false" + app:sessionsListHeaderMenu="@menu/menu_current_session_header" app:sessionsListHeaderTitle="@string/device_manager_current_session_title" /> + + + + + From 2b8dc13dcad821517cdee725895762fd39c59676 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 2 Dec 2022 17:11:10 +0100 Subject: [PATCH 052/197] Adding listener on the new menu item --- .../devices/v2/VectorSettingsDevicesFragment.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index c3715813951..a2083626800 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -99,6 +99,7 @@ class VectorSettingsDevicesFragment : super.onViewCreated(view, savedInstanceState) initWaitingView() + initCurrentSessionHeaderView() initOtherSessionsHeaderView() initOtherSessionsView() initSecurityRecommendationsView() @@ -139,6 +140,18 @@ class VectorSettingsDevicesFragment : views.waitingView.waitingStatusText.isVisible = true } + private fun initCurrentSessionHeaderView() { + views.deviceListHeaderCurrentSession.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.currentSessionHeaderSignoutOtherSessions -> { + confirmMultiSignoutOtherSessions() + true + } + else -> false + } + } + } + private fun initOtherSessionsHeaderView() { views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { @@ -327,7 +340,7 @@ class VectorSettingsDevicesFragment : } else { stringProvider.getString(R.string.device_manager_other_sessions_show_ip_address) } - } + } } private fun hideOtherSessionsView() { From efc436c3f5b878b115f75b88b3d3caec7896b70c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 2 Dec 2022 17:17:44 +0100 Subject: [PATCH 053/197] Hide the action when there are no other sessions --- .../settings/devices/v2/VectorSettingsDevicesFragment.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index a2083626800..d748600416d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -53,6 +53,7 @@ import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationVie import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject @@ -260,7 +261,7 @@ class VectorSettingsDevicesFragment : val otherDevices = devices?.filter { it.deviceInfo.deviceId != currentDeviceId } renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount, isCurrentSessionVerified) - renderCurrentSessionView(currentDeviceInfo) + renderCurrentSessionView(currentDeviceInfo, hasOtherDevices = otherDevices?.isNotEmpty().orFalse()) renderOtherSessionsView(otherDevices, state.isShowingIpAddress) } else { hideSecurityRecommendations() @@ -348,12 +349,13 @@ class VectorSettingsDevicesFragment : views.deviceListOtherSessions.isVisible = false } - private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?) { + private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?, hasOtherDevices: Boolean) { currentDeviceInfo?.let { views.deviceListHeaderCurrentSession.isVisible = true val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError) val signoutOtherSessionsItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignoutOtherSessions) signoutOtherSessionsItem.setTextColor(colorDestructive) + signoutOtherSessionsItem.isVisible = hasOtherDevices views.deviceListCurrentSession.isVisible = true val viewState = SessionInfoViewState( isCurrentSession = true, From b8ab1b5620e071974d3600479b915e91ba72780c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 2 Dec 2022 17:35:13 +0100 Subject: [PATCH 054/197] Adding changelog entry --- changelog.d/7697.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7697.feature diff --git a/changelog.d/7697.feature b/changelog.d/7697.feature new file mode 100644 index 00000000000..6d71a84a40b --- /dev/null +++ b/changelog.d/7697.feature @@ -0,0 +1 @@ +[Session manager] Add actions to rename and signout current session From 980d59ab58caec7629ec0110fcf9236ccf100ba3 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 2 Dec 2022 21:21:12 +0300 Subject: [PATCH 055/197] Fix lint. --- library/ui-strings/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 3945a80393d..683b9f754d2 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -2648,9 +2648,9 @@ Encrypted by an unverified device The authenticity of this encrypted message can\'t be guaranteed on this device. - Review where you’re logged in + Review where you’re logged in - Verify all your sessions to ensure your account & messages are safe + Verify all your sessions to ensure your account & messages are safe You have unverified sessions Review to ensure your account is safe From 53ef97d9492c3aa674246918bdd8072f9be69b02 Mon Sep 17 00:00:00 2001 From: phardyle Date: Fri, 2 Dec 2022 10:05:03 +0000 Subject: [PATCH 056/197] Translated using Weblate (Chinese (Simplified)) Currently translated at 99.6% (2550 of 2558 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hans/ --- .../src/main/res/values-zh-rCN/strings.xml | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml index 5ab8a351d1b..0a01610c36b 100644 --- a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml @@ -1007,7 +1007,7 @@ 您当前在身份服务器 %1$s 上共享电子邮件地址或电话号码。您需要重新连接到 %2$s 才能停止共享它们。 同意身份服务器 (%s) 服务条款使你可以通过电子邮件地址或电话号码被发现。 启用详细日志。 - 详细日志将通过在您发送 RageShake 时提供更多日志来帮助开发人员。即使启用,应用程序也不会记录消息内容或任何其他私人数据。 + 详细日志将通过在您发送愤怒摇动(RageShake)时提供更多日志来帮助开发人员。即使启用,应用程序也不会记录消息内容或任何其他私人数据。 接收你的主服务器条款和条件后请重试。 服务器似乎响应时间太长,这可能是由于连接不良或服务器错误引起的。请稍后再试。 发送附件 @@ -1205,7 +1205,7 @@ 高级设置 开发者模式 开发者模式激活隐藏的功能,也可能使应用不稳定。仅供开发者使用! - 摇一摇 + 愤怒摇动(Rageshake) 检测阈值 摇动手机以测试检测阈值 检测到摇动! @@ -1213,7 +1213,7 @@ 当前会话 其它会话 仅显示第一个结果,请输入更多字符… - 快速失败 + 快速失败(Fail-fast) 发生意外错误时,${app_name} 可能更经常崩溃 在明文消息前添加 ¯\\_(ツ)_/¯ 启用加密 @@ -2694,7 +2694,7 @@ 验证您当前的会话以显示此会话的验证状态。 未知的验证状态 开始语音广播 - 缓冲 + 正在缓冲…… 暂停语音广播 实时 知道了 @@ -2789,4 +2789,19 @@ 已选择 %1$d + 已创建投票。 + 已发送贴纸。 + 已发送视频。 + 已发送图片。 + 已发送语音消息。 + 已发送音频文件。 + 已发送文件。 + 已验证的会话是在输入你的口令词组或用另一个已验证的会话确认你的身份之后你使用此账户的任何地方。 +\n +\n这意味着你拥有解锁你的已加密消息和向其他用户证明你信任此会话所需的全部密钥。 + + 登出%1$d个会话 + + 登出 + 剩余%1$s \ No newline at end of file From 6d1f9408c87d2b05de052307f2994300297d996c Mon Sep 17 00:00:00 2001 From: Vri Date: Fri, 2 Dec 2022 05:54:40 +0000 Subject: [PATCH 057/197] Translated using Weblate (German) Currently translated at 100.0% (83 of 83 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/de/ --- fastlane/metadata/android/de-DE/changelogs/40105100.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/40105100.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/40105100.txt b/fastlane/metadata/android/de-DE/changelogs/40105100.txt new file mode 100644 index 00000000000..de5f4d90e8d --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105100.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Der Vollbildmodus des Textverarbeitungseditors wurde neu umgesetzt und es wurden diverse Fehler behoben. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases From 098dee1aa7a55d47107ef91cdcb4e2664eac0ab9 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Thu, 1 Dec 2022 21:32:42 +0000 Subject: [PATCH 058/197] Translated using Weblate (Slovak) Currently translated at 100.0% (83 of 83 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/sk/ --- fastlane/metadata/android/sk/changelogs/40105100.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/sk/changelogs/40105100.txt diff --git a/fastlane/metadata/android/sk/changelogs/40105100.txt b/fastlane/metadata/android/sk/changelogs/40105100.txt new file mode 100644 index 00000000000..c286f155d4f --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105100.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Nová implementácia celo-obrazovkového režimu pre Rozšírený textový editor a opravy chýb. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases From 873fa2a210e299bc02fe0a4bf0b34d3662424df6 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Thu, 1 Dec 2022 09:57:42 +0000 Subject: [PATCH 059/197] Translated using Weblate (Ukrainian) Currently translated at 100.0% (83 of 83 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/uk/ --- fastlane/metadata/android/uk/changelogs/40105100.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/40105100.txt diff --git a/fastlane/metadata/android/uk/changelogs/40105100.txt b/fastlane/metadata/android/uk/changelogs/40105100.txt new file mode 100644 index 00000000000..6bb3ab95c75 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105100.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Нова реалізація повноекранного режиму для редактора розширеного тексту та виправлення помилок. +Перелік усіх змін: https://github.com/vector-im/element-android/releases From 10d03e16afb7e291bad83bf0fd0cf98de23ea7d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Thu, 1 Dec 2022 16:57:40 +0000 Subject: [PATCH 060/197] Translated using Weblate (Estonian) Currently translated at 100.0% (83 of 83 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/et/ --- fastlane/metadata/android/et/changelogs/40105100.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/et/changelogs/40105100.txt diff --git a/fastlane/metadata/android/et/changelogs/40105100.txt b/fastlane/metadata/android/et/changelogs/40105100.txt new file mode 100644 index 00000000000..f6212db01b0 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105100.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: tekstitoimeti täisekraanivaade ja erinevate vigade parandused. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases From 61eb0d6b449ae62e7b50708c745d3b627274ac9b Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Fri, 2 Dec 2022 01:49:09 +0000 Subject: [PATCH 061/197] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (83 of 83 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/zh_Hant/ --- fastlane/metadata/android/zh-TW/changelogs/40105100.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/zh-TW/changelogs/40105100.txt diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105100.txt b/fastlane/metadata/android/zh-TW/changelogs/40105100.txt new file mode 100644 index 00000000000..20341b84fe7 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105100.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:格式化文字編輯器的全螢幕模式新實作與臭蟲修復。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases From 58d10e901e689a53803ad6cb3fe0c9f796d68fc1 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Thu, 1 Dec 2022 11:18:34 +0000 Subject: [PATCH 062/197] Translated using Weblate (Czech) Currently translated at 100.0% (83 of 83 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/cs/ --- fastlane/metadata/android/cs-CZ/changelogs/40105100.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/cs-CZ/changelogs/40105100.txt diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105100.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105100.txt new file mode 100644 index 00000000000..8c51742e06f --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105100.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Nová implementace celoobrazovkového režimu pro editor formátovaného textu a opravy chyb. +Úplný seznam změn: https://github.com/vector-im/element-android/releases From 70e9f13ec23f4c17a65d1c1bf85689e8ddd59ac1 Mon Sep 17 00:00:00 2001 From: Linerly Date: Thu, 1 Dec 2022 12:00:15 +0000 Subject: [PATCH 063/197] Translated using Weblate (Indonesian) Currently translated at 100.0% (83 of 83 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/id/ --- fastlane/metadata/android/id/changelogs/40105100.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/id/changelogs/40105100.txt diff --git a/fastlane/metadata/android/id/changelogs/40105100.txt b/fastlane/metadata/android/id/changelogs/40105100.txt new file mode 100644 index 00000000000..0c7d2f5262d --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105100.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Penerapan baru mode layar penuh untuk Penyunting Teks Kaya dan perbaikan kutu. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases From 9456789047f96d0fd2aa2313a2e2ff97a96f91da Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 28 Nov 2022 11:38:06 +0100 Subject: [PATCH 064/197] Adding changelog entry --- changelog.d/7653.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7653.bugfix diff --git a/changelog.d/7653.bugfix b/changelog.d/7653.bugfix new file mode 100644 index 00000000000..ae49c4ed4e6 --- /dev/null +++ b/changelog.d/7653.bugfix @@ -0,0 +1 @@ +ANR when asking to select the notification method From 4dbca7858cbb49d95135a34e2fc4f84330d75460 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 29 Nov 2022 15:07:26 +0100 Subject: [PATCH 065/197] Adding new use cases to handle the Unified push registration --- .../im/vector/app/push/fcm/FdroidFcmHelper.kt | 3 +- .../im/vector/app/push/fcm/GoogleFcmHelper.kt | 13 ++-- .../EnsureFcmTokenIsRetrievedUseCase.kt | 44 ++++++++++++ .../im/vector/app/core/pushers/FcmHelper.kt | 3 +- .../pushers/RegisterUnifiedPushUseCase.kt | 70 +++++++++++++++++++ .../app/core/pushers/UnifiedPushHelper.kt | 60 ++++++++++++++-- .../pushers/UnregisterUnifiedPushUseCase.kt | 51 ++++++++++++++ .../vector/app/features/home/HomeActivity.kt | 29 ++------ .../features/home/HomeActivityViewActions.kt | 1 + .../features/home/HomeActivityViewEvents.kt | 3 + .../features/home/HomeActivityViewModel.kt | 53 +++++++------- .../features/home/HomeActivityViewState.kt | 1 - ...leNotificationsForCurrentSessionUseCase.kt | 12 ++-- ...rSettingsNotificationPreferenceFragment.kt | 10 +-- 14 files changed, 271 insertions(+), 82 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt diff --git a/vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt b/vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt index 5b83769116a..44fd92953eb 100755 --- a/vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt +++ b/vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt @@ -17,7 +17,6 @@ package im.vector.app.push.fcm -import android.app.Activity import android.content.Context import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.FcmHelper @@ -44,7 +43,7 @@ class FdroidFcmHelper @Inject constructor( // No op } - override fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) { + override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) { // No op } diff --git a/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt b/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt index 7cf90cf874a..53e65f88b45 100755 --- a/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt +++ b/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt @@ -15,7 +15,6 @@ */ package im.vector.app.push.fcm -import android.app.Activity import android.content.Context import android.content.SharedPreferences import android.widget.Toast @@ -23,6 +22,7 @@ import androidx.core.content.edit import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.google.firebase.messaging.FirebaseMessaging +import dagger.hilt.android.qualifiers.ApplicationContext import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.DefaultPreferences @@ -36,8 +36,8 @@ import javax.inject.Inject * It has an alter ego in the fdroid variant. */ class GoogleFcmHelper @Inject constructor( - @DefaultPreferences - private val sharedPrefs: SharedPreferences, + @ApplicationContext private val context: Context, + @DefaultPreferences private val sharedPrefs: SharedPreferences, ) : FcmHelper { companion object { private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" @@ -56,10 +56,9 @@ class GoogleFcmHelper @Inject constructor( } } - override fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) { - // if (TextUtils.isEmpty(getFcmToken(activity))) { + override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) { // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' - if (checkPlayServices(activity)) { + if (checkPlayServices(context)) { try { FirebaseMessaging.getInstance().token .addOnSuccessListener { token -> @@ -75,7 +74,7 @@ class GoogleFcmHelper @Inject constructor( Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") } } else { - Toast.makeText(activity, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show() Timber.e("No valid Google Play Services found. Cannot use FCM.") } } diff --git a/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt new file mode 100644 index 00000000000..4a8ff5fb39c --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.pushers + +import im.vector.app.core.di.ActiveSessionHolder +import timber.log.Timber +import javax.inject.Inject + +class EnsureFcmTokenIsRetrievedUseCase @Inject constructor( + private val unifiedPushHelper: UnifiedPushHelper, + private val fcmHelper: FcmHelper, + private val activeSessionHolder: ActiveSessionHolder, +) { + + // TODO add unit tests + fun execute(pushersManager: PushersManager, registerPusher: Boolean) { + if (unifiedPushHelper.isEmbeddedDistributor()) { + Timber.d("ensureFcmTokenIsRetrieved") + fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher)) + } + } + + private fun shouldAddHttpPusher(registerPusher: Boolean) = if (registerPusher) { + val currentSession = activeSessionHolder.getActiveSession() + val currentPushers = currentSession.pushersService().getPushers() + currentPushers.none { it.deviceId == currentSession.sessionParams.deviceId } + } else { + false + } +} diff --git a/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt index 7b2c5e39595..0cc251ce31e 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt @@ -39,11 +39,10 @@ interface FcmHelper { /** * onNewToken may not be called on application upgrade, so ensure my shared pref is set. * - * @param activity the first launch Activity. * @param pushersManager the instance to register the pusher on. * @param registerPusher whether the pusher should be registered. */ - fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) + fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) fun onEnterForeground(activeSessionHolder: ActiveSessionHolder) diff --git a/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt new file mode 100644 index 00000000000..7aafa073488 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.pushers + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import im.vector.app.features.VectorFeatures +import org.unifiedpush.android.connector.UnifiedPush +import javax.inject.Inject + +class RegisterUnifiedPushUseCase @Inject constructor( + @ApplicationContext private val context: Context, + private val vectorFeatures: VectorFeatures, +) { + + sealed interface RegisterUnifiedPushResult { + object Success : RegisterUnifiedPushResult + data class NeedToAskUserForDistributor(val distributors: List) : RegisterUnifiedPushResult + } + + // TODO add unit tests + fun execute(distributor: String = ""): RegisterUnifiedPushResult { + if(distributor.isNotEmpty()) { + saveAndRegisterApp(distributor) + return RegisterUnifiedPushResult.Success + } + + if (!vectorFeatures.allowExternalUnifiedPushDistributors()) { + saveAndRegisterApp(context.packageName) + return RegisterUnifiedPushResult.Success + } + + if (UnifiedPush.getDistributor(context).isNotEmpty()) { + registerApp() + return RegisterUnifiedPushResult.Success + } + + val distributors = UnifiedPush.getDistributors(context) + + return if (distributors.size == 1) { + saveAndRegisterApp(distributors.first()) + RegisterUnifiedPushResult.Success + } else { + RegisterUnifiedPushResult.NeedToAskUserForDistributor(distributors) + } + } + + private fun saveAndRegisterApp(distributor: String) { + UnifiedPush.saveDistributor(context, distributor) + registerApp() + } + + private fun registerApp() { + UnifiedPush.registerApp(context) + } +} diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index aab94eca938..64d2a794946 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -17,6 +17,7 @@ package im.vector.app.core.pushers import android.content.Context +import androidx.annotation.MainThread import androidx.fragment.app.FragmentActivity import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -28,7 +29,9 @@ import im.vector.app.core.utils.getApplicationLabel import im.vector.app.features.VectorFeatures import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.api.util.MatrixJsonParser @@ -49,6 +52,7 @@ class UnifiedPushHelper @Inject constructor( // Called when the home activity starts // or when notifications are enabled + // TODO remove and replace by use case fun register( activity: FragmentActivity, onDoneRunnable: Runnable? = null, @@ -66,10 +70,11 @@ class UnifiedPushHelper @Inject constructor( // The registration is forced in 2 cases : // * in the settings // * in the troubleshoot list (doFix) + // TODO remove and replace by use case fun forceRegister( activity: FragmentActivity, pushersManager: PushersManager, - onDoneRunnable: Runnable? = null + @MainThread onDoneRunnable: Runnable? = null ) { registerInternal( activity, @@ -79,17 +84,21 @@ class UnifiedPushHelper @Inject constructor( ) } + // TODO remove private fun registerInternal( activity: FragmentActivity, force: Boolean = false, pushersManager: PushersManager? = null, onDoneRunnable: Runnable? = null ) { - activity.lifecycleScope.launch { + activity.lifecycleScope.launch(Dispatchers.IO) { + Timber.d("registerInternal force=$force, $activity on thread ${Thread.currentThread()}") if (!vectorFeatures.allowExternalUnifiedPushDistributors()) { UnifiedPush.saveDistributor(context, context.packageName) UnifiedPush.registerApp(context) - onDoneRunnable?.run() + withContext(Dispatchers.Main) { + onDoneRunnable?.run() + } return@launch } if (force) { @@ -99,7 +108,9 @@ class UnifiedPushHelper @Inject constructor( // the !force should not be needed if (!force && UnifiedPush.getDistributor(context).isNotEmpty()) { UnifiedPush.registerApp(context) - onDoneRunnable?.run() + withContext(Dispatchers.Main) { + onDoneRunnable?.run() + } return@launch } @@ -108,7 +119,9 @@ class UnifiedPushHelper @Inject constructor( if (!force && distributors.size == 1) { UnifiedPush.saveDistributor(context, distributors.first()) UnifiedPush.registerApp(context) - onDoneRunnable?.run() + withContext(Dispatchers.Main) { + onDoneRunnable?.run() + } } else { openDistributorDialogInternal( activity = activity, @@ -164,6 +177,43 @@ class UnifiedPushHelper @Inject constructor( .show() } + @MainThread + fun showSelectDistributorDialog( + context: Context, + distributors: List, + onDistributorSelected: (String) -> Unit, + ) { + val internalDistributorName = stringProvider.getString( + if (fcmHelper.isFirebaseAvailable()) { + R.string.unifiedpush_distributor_fcm_fallback + } else { + R.string.unifiedpush_distributor_background_sync + } + ) + + val distributorsName = distributors.map { + if (it == context.packageName) { + internalDistributorName + } else { + context.getApplicationLabel(it) + } + } + + MaterialAlertDialogBuilder(context) + .setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title)) + .setItems(distributorsName.toTypedArray()) { _, which -> + val distributor = distributors[which] + onDistributorSelected(distributor) + } + .setOnCancelListener { + // By default, use internal solution (fcm/background sync) + onDistributorSelected(context.packageName) + } + .setCancelable(true) + .show() + } + + // TODO remove and replace by use case suspend fun unregister(pushersManager: PushersManager? = null) { val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME vectorPreferences.setFdroidSyncBackgroundMode(mode) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt new file mode 100644 index 00000000000..d81581679ef --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.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.core.pushers + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import im.vector.app.features.settings.BackgroundSyncMode +import im.vector.app.features.settings.VectorPreferences +import org.unifiedpush.android.connector.UnifiedPush +import timber.log.Timber +import javax.inject.Inject + +class UnregisterUnifiedPushUseCase @Inject constructor( + @ApplicationContext private val context: Context, + private val pushersManager: PushersManager, + private val vectorPreferences: VectorPreferences, + private val unifiedPushStore: UnifiedPushStore, + private val unifiedPushHelper: UnifiedPushHelper, +) { + + // TODO add unit tests + suspend fun execute() { + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME + vectorPreferences.setFdroidSyncBackgroundMode(mode) + try { + unifiedPushHelper.getEndpointOrToken()?.let { + Timber.d("Removing $it") + pushersManager.unregisterPusher(it) + } + } catch (e: Exception) { + Timber.d(e, "Probably unregistering a non existing pusher") + } + unifiedPushStore.storeUpEndpoint(null) + unifiedPushStore.storePushGateway(null) + UnifiedPush.unregisterApp(context) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 2df94fecad6..14157a1de87 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -44,8 +44,6 @@ import im.vector.app.core.extensions.restart import im.vector.app.core.extensions.validateBackPressed import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorMenuProvider -import im.vector.app.core.pushers.FcmHelper -import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.startSharePlainTextIntent @@ -128,7 +126,6 @@ class HomeActivity : private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler - @Inject lateinit var pushersManager: PushersManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var shortcutsHandler: ShortcutsHandler @@ -137,7 +134,6 @@ class HomeActivity : @Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter @Inject lateinit var spaceStateHandler: SpaceStateHandler @Inject lateinit var unifiedPushHelper: UnifiedPushHelper - @Inject lateinit var fcmHelper: FcmHelper @Inject lateinit var nightlyProxy: NightlyProxy @Inject lateinit var disclaimerDialog: DisclaimerDialog @Inject lateinit var notificationPermissionManager: NotificationPermissionManager @@ -209,16 +205,6 @@ class HomeActivity : isNewAppLayoutEnabled = vectorPreferences.isNewAppLayoutEnabled() analyticsScreenName = MobileScreen.ScreenName.Home supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) - unifiedPushHelper.register(this) { - if (unifiedPushHelper.isEmbeddedDistributor()) { - fcmHelper.ensureFcmTokenIsRetrieved( - this, - pushersManager, - homeActivityViewModel.shouldAddHttpPusher() - ) - } - } - sharedActionViewModel = viewModelProvider[HomeSharedActionViewModel::class.java] roomListSharedActionViewModel = viewModelProvider[RoomListSharedActionViewModel::class.java] views.drawerLayout.addDrawerListener(drawerListener) @@ -280,6 +266,7 @@ class HomeActivity : HomeActivityViewEvents.ShowReleaseNotes -> handleShowReleaseNotes() HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration() is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession) + is HomeActivityViewEvents.AskUserForPushDistributor -> askUserToSelectPushDistributor(it.distributors) } } homeActivityViewModel.onEach { renderState(it) } @@ -292,6 +279,12 @@ class HomeActivity : homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted) } + private fun askUserToSelectPushDistributor(distributors: List) { + unifiedPushHelper.showSelectDistributorDialog(this, distributors) { selection -> + homeActivityViewModel.handle(HomeActivityViewActions.RegisterPushDistributor(selection)) + } + } + private fun handleShowNotificationDialog() { notificationPermissionManager.eventuallyRequestPermission(this, postPermissionLauncher) } @@ -415,14 +408,6 @@ class HomeActivity : } private fun renderState(state: HomeActivityViewState) { - lifecycleScope.launch { - if (state.areNotificationsSilenced) { - unifiedPushHelper.unregister(pushersManager) - } else { - unifiedPushHelper.register(this@HomeActivity) - } - } - when (val status = state.syncRequestState) { is SyncRequestState.InitialSyncProgressing -> { val initSyncStepStr = initSyncStepFormatter.format(status.initialSyncStep) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt index 5f89c89bc9f..54392d5f56d 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt @@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed interface HomeActivityViewActions : VectorViewModelAction { object ViewStarted : HomeActivityViewActions object PushPromptHasBeenReviewed : HomeActivityViewActions + data class RegisterPushDistributor(val distributor: String) : HomeActivityViewActions } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt index e548fdb2f3b..6fdf441d1d8 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt @@ -25,9 +25,11 @@ sealed interface HomeActivityViewEvents : VectorViewEvents { val userItem: MatrixItem.UserItem, val waitForIncomingRequest: Boolean = true, ) : HomeActivityViewEvents + data class CurrentSessionCannotBeVerified( val userItem: MatrixItem.UserItem, ) : HomeActivityViewEvents + data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents object PromptToEnableSessionPush : HomeActivityViewEvents object ShowAnalyticsOptIn : HomeActivityViewEvents @@ -37,4 +39,5 @@ sealed interface HomeActivityViewEvents : VectorViewEvents { data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents object StartRecoverySetupFlow : HomeActivityViewEvents data class ForceVerification(val sendRequest: Boolean) : HomeActivityViewEvents + data class AskUserForPushDistributor(val distributors: List) : HomeActivityViewEvents } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 49f2079625f..3fd555bbea3 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -16,7 +16,6 @@ package im.vector.app.features.home -import androidx.lifecycle.asFlow import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.ViewModelContext @@ -27,7 +26,9 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.VectorFeatures +import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase +import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.RegisterUnifiedPushUseCase import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsType @@ -48,12 +49,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserPasswordAuth @@ -62,11 +61,9 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.session.pushrules.RuleIds import org.matrix.android.sdk.api.session.room.model.Membership @@ -92,8 +89,10 @@ class HomeActivityViewModel @AssistedInject constructor( private val analyticsTracker: AnalyticsTracker, private val analyticsConfig: AnalyticsConfig, private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore, - private val vectorFeatures: VectorFeatures, private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase, + private val pushersManager: PushersManager, + private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, + private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase, ) : VectorViewModel(initialState) { @AssistedFactory @@ -117,17 +116,32 @@ class HomeActivityViewModel @AssistedInject constructor( private fun initialize() { if (isInitialized) return isInitialized = true + registerUnifiedPush(distributor = "") cleanupFiles() observeInitialSync() checkSessionPushIsOn() observeCrossSigningReset() observeAnalytics() observeReleaseNotes() - observeLocalNotificationsSilenced() initThreadsMigration() viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() } } + private fun registerUnifiedPush(distributor: String) { + viewModelScope.launch { + when (val result = registerUnifiedPushUseCase.execute(distributor = distributor)) { + is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> { + Timber.d("registerUnifiedPush $distributor need to ask user") + _viewEvents.post(HomeActivityViewEvents.AskUserForPushDistributor(result.distributors)) + } + RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> { + Timber.d("registerUnifiedPush $distributor success") + ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice()) + } + } + } + } + private fun observeReleaseNotes() = withState { state -> if (vectorPreferences.isNewAppLayoutEnabled()) { // we don't want to show release notes for new users or after relogin @@ -146,26 +160,6 @@ class HomeActivityViewModel @AssistedInject constructor( } } - fun shouldAddHttpPusher() = if (vectorPreferences.areNotificationEnabledForDevice()) { - val currentSession = activeSessionHolder.getActiveSession() - val currentPushers = currentSession.pushersService().getPushers() - currentPushers.none { it.deviceId == currentSession.sessionParams.deviceId } - } else { - false - } - - fun observeLocalNotificationsSilenced() { - val currentSession = activeSessionHolder.getActiveSession() - val deviceId = currentSession.cryptoService().getMyDevice().deviceId - viewModelScope.launch { - currentSession.accountDataService() - .getLiveUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) - .asFlow() - .map { it.getOrNull()?.content?.toModel()?.isSilenced ?: false } - .onEach { setState { copy(areNotificationsSilenced = it) } } - } - } - private fun observeAnalytics() { if (analyticsConfig.isEnabled) { analyticsStore.didAskUserConsentFlow @@ -501,6 +495,9 @@ class HomeActivityViewModel @AssistedInject constructor( HomeActivityViewActions.ViewStarted -> { initialize() } + is HomeActivityViewActions.RegisterPushDistributor -> { + registerUnifiedPush(distributor = action.distributor) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt index 4df2957cbcf..f9c1b37ed5d 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt @@ -23,5 +23,4 @@ import org.matrix.android.sdk.api.session.sync.SyncRequestState data class HomeActivityViewState( val syncRequestState: SyncRequestState = SyncRequestState.Idle, val authenticationDescription: AuthenticationDescription? = null, - val areNotificationsSilenced: Boolean = false, ) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt index 180627a15f4..91974787bd8 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt @@ -18,6 +18,7 @@ package im.vector.app.features.settings.notifications import androidx.fragment.app.FragmentActivity import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase import im.vector.app.core.pushers.FcmHelper import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnifiedPushHelper @@ -32,11 +33,12 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, private val unifiedPushHelper: UnifiedPushHelper, private val pushersManager: PushersManager, - private val fcmHelper: FcmHelper, private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, private val togglePushNotificationUseCase: TogglePushNotificationUseCase, + private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase, ) { + // TODO update unit tests suspend fun execute(fragmentActivity: FragmentActivity) { val pusherForCurrentSession = pushersManager.getPusherForCurrentSession() if (pusherForCurrentSession == null) { @@ -54,13 +56,7 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor( suspendCoroutine { continuation -> try { unifiedPushHelper.register(fragmentActivity) { - if (unifiedPushHelper.isEmbeddedDistributor()) { - fcmHelper.ensureFcmTokenIsRetrieved( - fragmentActivity, - pushersManager, - registerPusher = true - ) - } + ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = true) continuation.resume(Unit) } } catch (error: Exception) { diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 58f86bc949c..f8c1a9ad443 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -37,6 +37,7 @@ import im.vector.app.core.preference.VectorEditTextPreference import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreferenceCategory import im.vector.app.core.preference.VectorSwitchPreference +import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase import im.vector.app.core.pushers.FcmHelper import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnifiedPushHelper @@ -82,6 +83,7 @@ class VectorSettingsNotificationPreferenceFragment : @Inject lateinit var notificationPermissionManager: NotificationPermissionManager @Inject lateinit var disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase @Inject lateinit var enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase + @Inject lateinit var ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase override var titleRes: Int = R.string.settings_notifications override val preferenceXmlRes = R.xml.vector_settings_notifications @@ -183,13 +185,7 @@ class VectorSettingsNotificationPreferenceFragment : it.summary = unifiedPushHelper.getCurrentDistributorName() it.onPreferenceClickListener = Preference.OnPreferenceClickListener { unifiedPushHelper.forceRegister(requireActivity(), pushersManager) { - if (unifiedPushHelper.isEmbeddedDistributor()) { - fcmHelper.ensureFcmTokenIsRetrieved( - requireActivity(), - pushersManager, - vectorPreferences.areNotificationEnabledForDevice() - ) - } + ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice()) it.summary = unifiedPushHelper.getCurrentDistributorName() session.pushersService().refreshPushers() refreshBackgroundSyncPrefs() From 2890f41f30d675b9988d12e62bcd94916da04e3c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 29 Nov 2022 17:00:44 +0100 Subject: [PATCH 066/197] Replacing unregister method by usecase --- .../main/java/im/vector/app/core/di/ActiveSessionHolder.kt | 6 +++--- .../java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 2 +- .../app/core/pushers/UnregisterUnifiedPushUseCase.kt | 5 ++--- .../DisableNotificationsForCurrentSessionUseCase.kt | 7 ++++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index f1863cfa237..fead1e15b1c 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -19,7 +19,7 @@ package im.vector.app.core.di import android.content.Context import im.vector.app.ActiveSessionDataSource import im.vector.app.core.extensions.startSyncing -import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.session.ConfigureAndStartSessionUseCase import im.vector.app.features.call.webrtc.WebRtcCallManager @@ -46,12 +46,12 @@ class ActiveSessionHolder @Inject constructor( private val pushRuleTriggerListener: PushRuleTriggerListener, private val sessionListener: SessionListener, private val imageManager: ImageManager, - private val unifiedPushHelper: UnifiedPushHelper, private val guardServiceStarter: GuardServiceStarter, private val sessionInitializer: SessionInitializer, private val applicationContext: Context, private val authenticationService: AuthenticationService, private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase, + private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, ) { private var activeSessionReference: AtomicReference = AtomicReference() @@ -85,7 +85,7 @@ class ActiveSessionHolder @Inject constructor( incomingVerificationRequestHandler.stop() pushRuleTriggerListener.stop() // No need to unregister the pusher, the sign out will (should?) do it server side. - unifiedPushHelper.unregister(pushersManager = null) + unregisterUnifiedPushUseCase.execute(pushersManager = null) guardServiceStarter.stop() } diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 64d2a794946..34ba2542501 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -213,7 +213,7 @@ class UnifiedPushHelper @Inject constructor( .show() } - // TODO remove and replace by use case + // TODO remove suspend fun unregister(pushersManager: PushersManager? = null) { val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME vectorPreferences.setFdroidSyncBackgroundMode(mode) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt index d81581679ef..71b1a9c0339 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt @@ -26,20 +26,19 @@ import javax.inject.Inject class UnregisterUnifiedPushUseCase @Inject constructor( @ApplicationContext private val context: Context, - private val pushersManager: PushersManager, private val vectorPreferences: VectorPreferences, private val unifiedPushStore: UnifiedPushStore, private val unifiedPushHelper: UnifiedPushHelper, ) { // TODO add unit tests - suspend fun execute() { + suspend fun execute(pushersManager: PushersManager?) { val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME vectorPreferences.setFdroidSyncBackgroundMode(mode) try { unifiedPushHelper.getEndpointOrToken()?.let { Timber.d("Removing $it") - pushersManager.unregisterPusher(it) + pushersManager?.unregisterPusher(it) } } catch (e: Exception) { Timber.d(e, "Probably unregistering a non existing pusher") diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt index 61c884f0bcd..2ce2254f2e8 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt @@ -18,26 +18,27 @@ package im.vector.app.features.settings.notifications import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.PushersManager -import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import javax.inject.Inject class DisableNotificationsForCurrentSessionUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, - private val unifiedPushHelper: UnifiedPushHelper, private val pushersManager: PushersManager, private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, private val togglePushNotificationUseCase: TogglePushNotificationUseCase, + private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, ) { + // TODO update unit tests suspend fun execute() { val session = activeSessionHolder.getSafeActiveSession() ?: return val deviceId = session.sessionParams.deviceId ?: return if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { togglePushNotificationUseCase.execute(deviceId, enabled = false) } else { - unifiedPushHelper.unregister(pushersManager) + unregisterUnifiedPushUseCase.execute(pushersManager) } } } From 58efe90f7dfa23ffd17110eedec21ef480022fcf Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 29 Nov 2022 17:10:20 +0100 Subject: [PATCH 067/197] Removing some debug logs --- .../vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt | 2 -- .../java/im/vector/app/features/home/HomeActivityViewModel.kt | 2 -- 2 files changed, 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt index 4a8ff5fb39c..e55d0426ba1 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt @@ -17,7 +17,6 @@ package im.vector.app.core.pushers import im.vector.app.core.di.ActiveSessionHolder -import timber.log.Timber import javax.inject.Inject class EnsureFcmTokenIsRetrievedUseCase @Inject constructor( @@ -29,7 +28,6 @@ class EnsureFcmTokenIsRetrievedUseCase @Inject constructor( // TODO add unit tests fun execute(pushersManager: PushersManager, registerPusher: Boolean) { if (unifiedPushHelper.isEmbeddedDistributor()) { - Timber.d("ensureFcmTokenIsRetrieved") fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher)) } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 3fd555bbea3..2905decddfe 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -131,11 +131,9 @@ class HomeActivityViewModel @AssistedInject constructor( viewModelScope.launch { when (val result = registerUnifiedPushUseCase.execute(distributor = distributor)) { is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> { - Timber.d("registerUnifiedPush $distributor need to ask user") _viewEvents.post(HomeActivityViewEvents.AskUserForPushDistributor(result.distributors)) } RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> { - Timber.d("registerUnifiedPush $distributor success") ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice()) } } From b29191e892bbc9d0b2f15fa106c1524da7c4f180 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 29 Nov 2022 17:28:48 +0100 Subject: [PATCH 068/197] Using use cases inside component for endpoint testing --- .../TestEndpointAsTokenRegistration.kt | 63 ++++++++++++++----- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt index 3bbff0f2fe2..e6cb78d185f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt @@ -17,14 +17,17 @@ package im.vector.app.features.settings.troubleshoot import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.Observer import androidx.work.WorkInfo import androidx.work.WorkManager import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.RegisterUnifiedPushUseCase import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.core.resources.StringProvider +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.pushers.PusherState import javax.inject.Inject @@ -34,6 +37,8 @@ class TestEndpointAsTokenRegistration @Inject constructor( private val pushersManager: PushersManager, private val activeSessionHolder: ActiveSessionHolder, private val unifiedPushHelper: UnifiedPushHelper, + private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, + private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, ) : TroubleshootTest(R.string.settings_troubleshoot_test_endpoint_registration_title) { override fun perform(testParameters: TestParameters) { @@ -56,27 +61,53 @@ class TestEndpointAsTokenRegistration @Inject constructor( ) quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_endpoint_registration_quick_fix) { override fun doFix() { - unifiedPushHelper.forceRegister( - context, - pushersManager - ) - val workId = pushersManager.enqueueRegisterPusherWithFcmKey(endpoint) - WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo -> - if (workInfo != null) { - if (workInfo.state == WorkInfo.State.SUCCEEDED) { - manager?.retry(testParameters) - } else if (workInfo.state == WorkInfo.State.FAILED) { - manager?.retry(testParameters) - } - } - }) + unregisterThenRegister(testParameters, endpoint) } } - status = TestStatus.FAILED } else { description = stringProvider.getString(R.string.settings_troubleshoot_test_endpoint_registration_success) status = TestStatus.SUCCESS } } + + private fun unregisterThenRegister(testParameters: TestParameters, pushKey: String) { + activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch { + unregisterUnifiedPushUseCase.execute(pushersManager) + registerUnifiedPush(distributor = "", testParameters, pushKey) + } + } + + private fun registerUnifiedPush( + distributor: String, + testParameters: TestParameters, + pushKey: String, + ) { + when (val result = registerUnifiedPushUseCase.execute(distributor)) { + is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> + askUserForDistributor(result.distributors, testParameters, pushKey) + RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> { + val workId = pushersManager.enqueueRegisterPusherWithFcmKey(pushKey) + WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context) { workInfo -> + if (workInfo != null) { + if (workInfo.state == WorkInfo.State.SUCCEEDED) { + manager?.retry(testParameters) + } else if (workInfo.state == WorkInfo.State.FAILED) { + manager?.retry(testParameters) + } + } + } + } + } + } + + private fun askUserForDistributor( + distributors: List, + testParameters: TestParameters, + pushKey: String, + ) { + unifiedPushHelper.showSelectDistributorDialog(context, distributors) { selection -> + registerUnifiedPush(distributor = selection, testParameters, pushKey) + } + } } From 3f944e9d36c892e88676e3e64fb061afd98b158b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 30 Nov 2022 11:05:46 +0100 Subject: [PATCH 069/197] Extracting the logic to toggle notifications for device into a ViewModel --- .../app/core/di/MavericksViewModelModule.kt | 6 ++ .../settings/VectorSettingsBaseFragment.kt | 17 +++- ...leNotificationsForCurrentSessionUseCase.kt | 47 +++++------ ...rSettingsNotificationPreferenceFragment.kt | 73 ++++++++++++++---- ...ettingsNotificationPreferenceViewAction.kt | 25 ++++++ ...SettingsNotificationPreferenceViewEvent.kt | 26 +++++++ ...SettingsNotificationPreferenceViewModel.kt | 77 +++++++++++++++++++ 7 files changed, 226 insertions(+), 45 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 2242abb7aa3..ad3e3617750 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -105,6 +105,7 @@ import im.vector.app.features.settings.ignored.IgnoredUsersViewModel import im.vector.app.features.settings.labs.VectorSettingsLabsViewModel import im.vector.app.features.settings.legals.LegalsViewModel import im.vector.app.features.settings.locale.LocalePickerViewModel +import im.vector.app.features.settings.notifications.VectorSettingsNotificationPreferenceViewModel import im.vector.app.features.settings.push.PushGatewaysViewModel import im.vector.app.features.settings.threepids.ThreePidsSettingsViewModel import im.vector.app.features.share.IncomingShareViewModel @@ -683,4 +684,9 @@ interface MavericksViewModelModule { @IntoMap @MavericksViewModelKey(AttachmentTypeSelectorViewModel::class) fun attachmentTypeSelectorViewModelFactory(factory: AttachmentTypeSelectorViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(VectorSettingsNotificationPreferenceViewModel::class) + fun vectorSettingsNotificationPreferenceViewModelFactory(factory: VectorSettingsNotificationPreferenceViewModel.Factory): MavericksAssistedViewModelFactory<*, *> } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt index 176909b48de..38ba949a492 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt @@ -28,6 +28,8 @@ import im.vector.app.R import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.utils.toast import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.MobileScreen @@ -60,6 +62,19 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick protected lateinit var session: Session protected lateinit var errorFormatter: ErrorFormatter + /* ========================================================================================== + * ViewEvents + * ========================================================================================== */ + + protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { + viewEvents + .stream() + .onEach { + observer(it) + } + .launchIn(viewLifecycleOwner.lifecycleScope) + } + /* ========================================================================================== * Views * ========================================================================================== */ @@ -148,7 +163,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick } } - protected fun displayErrorDialog(throwable: Throwable) { + protected fun displayErrorDialog(throwable: Throwable?) { displayErrorDialog(errorFormatter.toHumanReadable(throwable)) } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt index 91974787bd8..2f8bdd4d0db 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt @@ -16,52 +16,45 @@ package im.vector.app.features.settings.notifications -import androidx.fragment.app.FragmentActivity import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase -import im.vector.app.core.pushers.FcmHelper import im.vector.app.core.pushers.PushersManager -import im.vector.app.core.pushers.UnifiedPushHelper -import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase +import im.vector.app.core.pushers.RegisterUnifiedPushUseCase import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import javax.inject.Inject -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine class EnableNotificationsForCurrentSessionUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, - private val unifiedPushHelper: UnifiedPushHelper, private val pushersManager: PushersManager, - private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, private val togglePushNotificationUseCase: TogglePushNotificationUseCase, + private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase, ) { + sealed interface EnableNotificationsResult { + object Success : EnableNotificationsResult + object Failure : EnableNotificationsResult + data class NeedToAskUserForDistributor(val distributors: List) : EnableNotificationsResult + } + // TODO update unit tests - suspend fun execute(fragmentActivity: FragmentActivity) { + suspend fun execute(distributor: String = ""): EnableNotificationsResult { val pusherForCurrentSession = pushersManager.getPusherForCurrentSession() if (pusherForCurrentSession == null) { - registerPusher(fragmentActivity) - } - - val session = activeSessionHolder.getSafeActiveSession() ?: return - if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { - val deviceId = session.sessionParams.deviceId ?: return - togglePushNotificationUseCase.execute(deviceId, enabled = true) - } - } - - private suspend fun registerPusher(fragmentActivity: FragmentActivity) { - suspendCoroutine { continuation -> - try { - unifiedPushHelper.register(fragmentActivity) { + when (val result = registerUnifiedPushUseCase.execute(distributor)) { + is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> { + return EnableNotificationsResult.NeedToAskUserForDistributor(result.distributors) + } + RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> { ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = true) - continuation.resume(Unit) } - } catch (error: Exception) { - continuation.resumeWithException(error) } } + + val session = activeSessionHolder.getSafeActiveSession() ?: return EnableNotificationsResult.Failure + val deviceId = session.sessionParams.deviceId ?: return EnableNotificationsResult.Failure + togglePushNotificationUseCase.execute(deviceId, enabled = true) + + return EnableNotificationsResult.Success } } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index f8c1a9ad443..ae00b3864ca 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -22,6 +22,7 @@ import android.content.Intent import android.media.RingtoneManager import android.net.Uri import android.os.Bundle +import android.view.View import android.widget.Toast import androidx.lifecycle.LiveData import androidx.lifecycle.distinctUntilChanged @@ -29,6 +30,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.map import androidx.preference.Preference import androidx.preference.SwitchPreference +import com.airbnb.mvrx.fragmentViewModel import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder @@ -81,8 +83,6 @@ class VectorSettingsNotificationPreferenceFragment : @Inject lateinit var guardServiceStarter: GuardServiceStarter @Inject lateinit var vectorFeatures: VectorFeatures @Inject lateinit var notificationPermissionManager: NotificationPermissionManager - @Inject lateinit var disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase - @Inject lateinit var enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase @Inject lateinit var ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase override var titleRes: Int = R.string.settings_notifications @@ -90,6 +90,8 @@ class VectorSettingsNotificationPreferenceFragment : private var interactionListener: VectorSettingsFragmentInteractionListener? = null + private val viewModel: VectorSettingsNotificationPreferenceViewModel by fragmentViewModel() + private val notificationStartForActivityResult = registerStartForActivityResult { _ -> // No op } @@ -106,6 +108,22 @@ class VectorSettingsNotificationPreferenceFragment : analyticsScreenName = MobileScreen.ScreenName.SettingsNotifications } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + observeViewEvents() + } + + private fun observeViewEvents() { + viewModel.observeViewEvents { + when (it) { + VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceEnabled -> onNotificationsForDeviceEnabled() + VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceDisabled -> onNotificationsForDeviceDisabled() + is VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor -> askUserToSelectPushDistributor(it.distributors) + VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure -> displayErrorDialog(throwable = null) + } + } + } + override fun bindPref() { findPreference(VectorPreferences.SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY)!!.let { pref -> val pushRuleService = session.pushRuleService() @@ -123,23 +141,15 @@ class VectorSettingsNotificationPreferenceFragment : } findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) - ?.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> - if (isChecked) { - enableNotificationsForCurrentSessionUseCase.execute(requireActivity()) - - findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) - ?.summary = unifiedPushHelper.getCurrentDistributorName() - - notificationPermissionManager.eventuallyRequestPermission( - requireActivity(), - postPermissionLauncher, - showRationale = false, - ignorePreference = true - ) + ?.setOnPreferenceChangeListener { _, isChecked -> + val action = if (isChecked as Boolean) { + VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(pushDistributor = "") } else { - disableNotificationsForCurrentSessionUseCase.execute() - notificationPermissionManager.eventuallyRevokePermission(requireActivity()) + VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice } + viewModel.handle(action) + // preference will be updated on ViewEvent reception + false } findPreference(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE)?.let { @@ -184,6 +194,8 @@ class VectorSettingsNotificationPreferenceFragment : if (vectorFeatures.allowExternalUnifiedPushDistributors()) { it.summary = unifiedPushHelper.getCurrentDistributorName() it.onPreferenceClickListener = Preference.OnPreferenceClickListener { + // TODO show dialog to pick a distributor + // TODO call unregister then register only when a new distributor has been selected => use UnifiedPushHelper method unifiedPushHelper.forceRegister(requireActivity(), pushersManager) { ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice()) it.summary = unifiedPushHelper.getCurrentDistributorName() @@ -203,6 +215,33 @@ class VectorSettingsNotificationPreferenceFragment : handleSystemPreference() } + private fun onNotificationsForDeviceEnabled() { + findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) + ?.isChecked = true + findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) + ?.summary = unifiedPushHelper.getCurrentDistributorName() + + notificationPermissionManager.eventuallyRequestPermission( + requireActivity(), + postPermissionLauncher, + showRationale = false, + ignorePreference = true + ) + } + + private fun onNotificationsForDeviceDisabled() { + findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) + ?.isChecked = false + notificationPermissionManager.eventuallyRevokePermission(requireActivity()) + } + + // TODO add an argument to know if unregister should be called + private fun askUserToSelectPushDistributor(distributors: List) { + unifiedPushHelper.showSelectDistributorDialog(requireContext(), distributors) { selection -> + viewModel.handle(VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(selection)) + } + } + private fun bindEmailNotifications() { val initialEmails = session.getEmailsWithPushInformation() bindEmailNotificationCategory(initialEmails) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewAction.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewAction.kt new file mode 100644 index 00000000000..949dc999934 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewAction.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.notifications + +import im.vector.app.core.platform.VectorViewModelAction + +sealed interface VectorSettingsNotificationPreferenceViewAction : VectorViewModelAction { + data class EnableNotificationsForDevice(val pushDistributor: String) : VectorSettingsNotificationPreferenceViewAction + object DisableNotificationsForDevice : VectorSettingsNotificationPreferenceViewAction + data class RegisterPushDistributor(val pushDistributor: String) : VectorSettingsNotificationPreferenceViewAction +} diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt new file mode 100644 index 00000000000..4948ad6e58a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.notifications + +import im.vector.app.core.platform.VectorViewEvents + +sealed interface VectorSettingsNotificationPreferenceViewEvent : VectorViewEvents { + object NotificationForDeviceEnabled : VectorSettingsNotificationPreferenceViewEvent + object EnableNotificationForDeviceFailure : VectorSettingsNotificationPreferenceViewEvent + object NotificationForDeviceDisabled : VectorSettingsNotificationPreferenceViewEvent + data class AskUserForPushDistributor(val distributors: List) : VectorSettingsNotificationPreferenceViewEvent +} diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt new file mode 100644 index 00000000000..0173f4846f9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.notifications + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.VectorDummyViewState +import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.launch + +class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor( + @Assisted initialState: VectorDummyViewState, + private val enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase, + private val disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase, +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: VectorDummyViewState): VectorSettingsNotificationPreferenceViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + // TODO add unit tests + override fun handle(action: VectorSettingsNotificationPreferenceViewAction) { + when (action) { + VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice -> handleDisableNotificationsForDevice() + is VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice -> handleEnableNotificationsForDevice(action.pushDistributor) + is VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor -> handleRegisterPushDistributor(action.pushDistributor) + } + } + + private fun handleDisableNotificationsForDevice() { + viewModelScope.launch { + disableNotificationsForCurrentSessionUseCase.execute() + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceDisabled) + } + } + + private fun handleEnableNotificationsForDevice(distributor: String) { + viewModelScope.launch { + when (val result = enableNotificationsForCurrentSessionUseCase.execute(distributor)) { + EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure -> { + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure) + } + is EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor -> { + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor(result.distributors)) + } + EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Success -> { + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceEnabled) + } + } + } + } + + private fun handleRegisterPushDistributor(distributor: String) { + handleEnableNotificationsForDevice(distributor) + } +} From 95556d25515d08b6f9fcc8154be65908110f17ef Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 30 Nov 2022 11:06:24 +0100 Subject: [PATCH 070/197] Change the distributor in dialog cancellation only if there is no existing one --- .../java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 34ba2542501..87231d1d67d 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -206,8 +206,11 @@ class UnifiedPushHelper @Inject constructor( onDistributorSelected(distributor) } .setOnCancelListener { - // By default, use internal solution (fcm/background sync) - onDistributorSelected(context.packageName) + // we do not want to change the distributor on behalf of the user + if (UnifiedPush.getDistributor(context).isEmpty()) { + // By default, use internal solution (fcm/background sync) + onDistributorSelected(context.packageName) + } } .setCancelable(true) .show() From 2673979ef8f7feddd60dcd6058a3790435729db0 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 30 Nov 2022 12:04:52 +0100 Subject: [PATCH 071/197] Handling change of notification method --- .../pushers/RegisterUnifiedPushUseCase.kt | 6 ++-- .../app/core/pushers/UnifiedPushHelper.kt | 7 ++-- .../vector/app/features/home/HomeActivity.kt | 6 ++-- .../features/home/HomeActivityViewEvents.kt | 2 +- .../features/home/HomeActivityViewModel.kt | 4 +-- ...leNotificationsForCurrentSessionUseCase.kt | 6 ++-- ...rSettingsNotificationPreferenceFragment.kt | 33 ++++++++++--------- ...SettingsNotificationPreferenceViewEvent.kt | 7 ++-- ...SettingsNotificationPreferenceViewModel.kt | 31 ++++++++++++++--- .../TestEndpointAsTokenRegistration.kt | 7 ++-- 10 files changed, 67 insertions(+), 42 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt index 7aafa073488..58bf0f50504 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt @@ -29,12 +29,12 @@ class RegisterUnifiedPushUseCase @Inject constructor( sealed interface RegisterUnifiedPushResult { object Success : RegisterUnifiedPushResult - data class NeedToAskUserForDistributor(val distributors: List) : RegisterUnifiedPushResult + object NeedToAskUserForDistributor : RegisterUnifiedPushResult } // TODO add unit tests fun execute(distributor: String = ""): RegisterUnifiedPushResult { - if(distributor.isNotEmpty()) { + if (distributor.isNotEmpty()) { saveAndRegisterApp(distributor) return RegisterUnifiedPushResult.Success } @@ -55,7 +55,7 @@ class RegisterUnifiedPushUseCase @Inject constructor( saveAndRegisterApp(distributors.first()) RegisterUnifiedPushResult.Success } else { - RegisterUnifiedPushResult.NeedToAskUserForDistributor(distributors) + RegisterUnifiedPushResult.NeedToAskUserForDistributor } } diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 87231d1d67d..efa396a9809 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -52,7 +52,7 @@ class UnifiedPushHelper @Inject constructor( // Called when the home activity starts // or when notifications are enabled - // TODO remove and replace by use case + // TODO remove fun register( activity: FragmentActivity, onDoneRunnable: Runnable? = null, @@ -70,7 +70,7 @@ class UnifiedPushHelper @Inject constructor( // The registration is forced in 2 cases : // * in the settings // * in the troubleshoot list (doFix) - // TODO remove and replace by use case + // TODO remove fun forceRegister( activity: FragmentActivity, pushersManager: PushersManager, @@ -132,6 +132,7 @@ class UnifiedPushHelper @Inject constructor( } } + // TODO remove // There is no case where this function is called // with a saved distributor and/or a pusher private fun openDistributorDialogInternal( @@ -180,7 +181,6 @@ class UnifiedPushHelper @Inject constructor( @MainThread fun showSelectDistributorDialog( context: Context, - distributors: List, onDistributorSelected: (String) -> Unit, ) { val internalDistributorName = stringProvider.getString( @@ -191,6 +191,7 @@ class UnifiedPushHelper @Inject constructor( } ) + val distributors = UnifiedPush.getDistributors(context) val distributorsName = distributors.map { if (it == context.packageName) { internalDistributorName diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 14157a1de87..8c6daae95a1 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -266,7 +266,7 @@ class HomeActivity : HomeActivityViewEvents.ShowReleaseNotes -> handleShowReleaseNotes() HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration() is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession) - is HomeActivityViewEvents.AskUserForPushDistributor -> askUserToSelectPushDistributor(it.distributors) + is HomeActivityViewEvents.AskUserForPushDistributor -> askUserToSelectPushDistributor() } } homeActivityViewModel.onEach { renderState(it) } @@ -279,8 +279,8 @@ class HomeActivity : homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted) } - private fun askUserToSelectPushDistributor(distributors: List) { - unifiedPushHelper.showSelectDistributorDialog(this, distributors) { selection -> + private fun askUserToSelectPushDistributor() { + unifiedPushHelper.showSelectDistributorDialog(this) { selection -> homeActivityViewModel.handle(HomeActivityViewActions.RegisterPushDistributor(selection)) } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt index 6fdf441d1d8..be5aa7def0c 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt @@ -39,5 +39,5 @@ sealed interface HomeActivityViewEvents : VectorViewEvents { data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents object StartRecoverySetupFlow : HomeActivityViewEvents data class ForceVerification(val sendRequest: Boolean) : HomeActivityViewEvents - data class AskUserForPushDistributor(val distributors: List) : HomeActivityViewEvents + object AskUserForPushDistributor : HomeActivityViewEvents } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 2905decddfe..7ffc46218c2 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -129,9 +129,9 @@ class HomeActivityViewModel @AssistedInject constructor( private fun registerUnifiedPush(distributor: String) { viewModelScope.launch { - when (val result = registerUnifiedPushUseCase.execute(distributor = distributor)) { + when (registerUnifiedPushUseCase.execute(distributor = distributor)) { is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> { - _viewEvents.post(HomeActivityViewEvents.AskUserForPushDistributor(result.distributors)) + _viewEvents.post(HomeActivityViewEvents.AskUserForPushDistributor) } RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> { ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice()) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt index 2f8bdd4d0db..e0b0a872f80 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt @@ -34,16 +34,16 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor( sealed interface EnableNotificationsResult { object Success : EnableNotificationsResult object Failure : EnableNotificationsResult - data class NeedToAskUserForDistributor(val distributors: List) : EnableNotificationsResult + object NeedToAskUserForDistributor : EnableNotificationsResult } // TODO update unit tests suspend fun execute(distributor: String = ""): EnableNotificationsResult { val pusherForCurrentSession = pushersManager.getPusherForCurrentSession() if (pusherForCurrentSession == null) { - when (val result = registerUnifiedPushUseCase.execute(distributor)) { + when (registerUnifiedPushUseCase.execute(distributor)) { is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> { - return EnableNotificationsResult.NeedToAskUserForDistributor(result.distributors) + return EnableNotificationsResult.NeedToAskUserForDistributor } RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> { ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = true) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index ae00b3864ca..238ed4218ce 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -116,10 +116,11 @@ class VectorSettingsNotificationPreferenceFragment : private fun observeViewEvents() { viewModel.observeViewEvents { when (it) { - VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceEnabled -> onNotificationsForDeviceEnabled() - VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceDisabled -> onNotificationsForDeviceDisabled() - is VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor -> askUserToSelectPushDistributor(it.distributors) + VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled -> onNotificationsForDeviceEnabled() + VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled -> onNotificationsForDeviceDisabled() + is VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor -> askUserToSelectPushDistributor() VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure -> displayErrorDialog(throwable = null) + VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged -> onNotificationMethodChanged() } } } @@ -194,14 +195,7 @@ class VectorSettingsNotificationPreferenceFragment : if (vectorFeatures.allowExternalUnifiedPushDistributors()) { it.summary = unifiedPushHelper.getCurrentDistributorName() it.onPreferenceClickListener = Preference.OnPreferenceClickListener { - // TODO show dialog to pick a distributor - // TODO call unregister then register only when a new distributor has been selected => use UnifiedPushHelper method - unifiedPushHelper.forceRegister(requireActivity(), pushersManager) { - ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice()) - it.summary = unifiedPushHelper.getCurrentDistributorName() - session.pushersService().refreshPushers() - refreshBackgroundSyncPrefs() - } + askUserToSelectPushDistributor(withUnregister = true) true } } else { @@ -235,13 +229,22 @@ class VectorSettingsNotificationPreferenceFragment : notificationPermissionManager.eventuallyRevokePermission(requireActivity()) } - // TODO add an argument to know if unregister should be called - private fun askUserToSelectPushDistributor(distributors: List) { - unifiedPushHelper.showSelectDistributorDialog(requireContext(), distributors) { selection -> - viewModel.handle(VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(selection)) + private fun askUserToSelectPushDistributor(withUnregister: Boolean = false) { + unifiedPushHelper.showSelectDistributorDialog(requireContext()) { selection -> + if (withUnregister) { + viewModel.handle(VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(selection)) + } else { + viewModel.handle(VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(selection)) + } } } + private fun onNotificationMethodChanged() { + findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY)?.summary = unifiedPushHelper.getCurrentDistributorName() + session.pushersService().refreshPushers() + refreshBackgroundSyncPrefs() + } + private fun bindEmailNotifications() { val initialEmails = session.getEmailsWithPushInformation() bindEmailNotificationCategory(initialEmails) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt index 4948ad6e58a..e4cf8e19733 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt @@ -19,8 +19,9 @@ package im.vector.app.features.settings.notifications import im.vector.app.core.platform.VectorViewEvents sealed interface VectorSettingsNotificationPreferenceViewEvent : VectorViewEvents { - object NotificationForDeviceEnabled : VectorSettingsNotificationPreferenceViewEvent + object NotificationsForDeviceEnabled : VectorSettingsNotificationPreferenceViewEvent object EnableNotificationForDeviceFailure : VectorSettingsNotificationPreferenceViewEvent - object NotificationForDeviceDisabled : VectorSettingsNotificationPreferenceViewEvent - data class AskUserForPushDistributor(val distributors: List) : VectorSettingsNotificationPreferenceViewEvent + object NotificationsForDeviceDisabled : VectorSettingsNotificationPreferenceViewEvent + object AskUserForPushDistributor : VectorSettingsNotificationPreferenceViewEvent + object NotificationMethodChanged : VectorSettingsNotificationPreferenceViewEvent } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt index 0173f4846f9..59c26749c9c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt @@ -24,12 +24,22 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorDummyViewState import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase +import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.RegisterUnifiedPushUseCase +import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase +import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.launch class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor( @Assisted initialState: VectorDummyViewState, + private val pushersManager: PushersManager, + private val vectorPreferences: VectorPreferences, private val enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase, private val disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase, + private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, + private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, + private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase, ) : VectorViewModel(initialState) { @AssistedFactory @@ -51,27 +61,38 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor( private fun handleDisableNotificationsForDevice() { viewModelScope.launch { disableNotificationsForCurrentSessionUseCase.execute() - _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceDisabled) + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled) } } private fun handleEnableNotificationsForDevice(distributor: String) { viewModelScope.launch { - when (val result = enableNotificationsForCurrentSessionUseCase.execute(distributor)) { + when (enableNotificationsForCurrentSessionUseCase.execute(distributor)) { EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure -> { _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure) } is EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor -> { - _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor(result.distributors)) + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor) } EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Success -> { - _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceEnabled) + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled) } } } } private fun handleRegisterPushDistributor(distributor: String) { - handleEnableNotificationsForDevice(distributor) + viewModelScope.launch { + unregisterUnifiedPushUseCase.execute(pushersManager) + when (registerUnifiedPushUseCase.execute(distributor)) { + RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> { + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor) + } + RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> { + ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice()) + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged) + } + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt index e6cb78d185f..b355b559039 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt @@ -83,9 +83,9 @@ class TestEndpointAsTokenRegistration @Inject constructor( testParameters: TestParameters, pushKey: String, ) { - when (val result = registerUnifiedPushUseCase.execute(distributor)) { + when (registerUnifiedPushUseCase.execute(distributor)) { is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> - askUserForDistributor(result.distributors, testParameters, pushKey) + askUserForDistributor(testParameters, pushKey) RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> { val workId = pushersManager.enqueueRegisterPusherWithFcmKey(pushKey) WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context) { workInfo -> @@ -102,11 +102,10 @@ class TestEndpointAsTokenRegistration @Inject constructor( } private fun askUserForDistributor( - distributors: List, testParameters: TestParameters, pushKey: String, ) { - unifiedPushHelper.showSelectDistributorDialog(context, distributors) { selection -> + unifiedPushHelper.showSelectDistributorDialog(context) { selection -> registerUnifiedPush(distributor = selection, testParameters, pushKey) } } From 740ed8963892ff76a4482c43c090996ca7579f76 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 30 Nov 2022 14:27:18 +0100 Subject: [PATCH 072/197] Removing the old methods from helper --- .../app/core/pushers/UnifiedPushHelper.kt | 155 ------------------ 1 file changed, 155 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index efa396a9809..9f96f13ee78 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -18,20 +18,12 @@ package im.vector.app.core.pushers import android.content.Context import androidx.annotation.MainThread -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.getApplicationLabel -import im.vector.app.features.VectorFeatures -import im.vector.app.features.settings.BackgroundSyncMode -import im.vector.app.features.settings.VectorPreferences -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.api.util.MatrixJsonParser @@ -44,140 +36,10 @@ class UnifiedPushHelper @Inject constructor( private val context: Context, private val unifiedPushStore: UnifiedPushStore, private val stringProvider: StringProvider, - private val vectorPreferences: VectorPreferences, private val matrix: Matrix, - private val vectorFeatures: VectorFeatures, private val fcmHelper: FcmHelper, ) { - // Called when the home activity starts - // or when notifications are enabled - // TODO remove - fun register( - activity: FragmentActivity, - onDoneRunnable: Runnable? = null, - ) { - registerInternal( - activity, - onDoneRunnable = onDoneRunnable - ) - } - - // If registration is forced: - // * the current distributor (if any) is removed - // * The dialog is opened - // - // The registration is forced in 2 cases : - // * in the settings - // * in the troubleshoot list (doFix) - // TODO remove - fun forceRegister( - activity: FragmentActivity, - pushersManager: PushersManager, - @MainThread onDoneRunnable: Runnable? = null - ) { - registerInternal( - activity, - force = true, - pushersManager = pushersManager, - onDoneRunnable = onDoneRunnable - ) - } - - // TODO remove - private fun registerInternal( - activity: FragmentActivity, - force: Boolean = false, - pushersManager: PushersManager? = null, - onDoneRunnable: Runnable? = null - ) { - activity.lifecycleScope.launch(Dispatchers.IO) { - Timber.d("registerInternal force=$force, $activity on thread ${Thread.currentThread()}") - if (!vectorFeatures.allowExternalUnifiedPushDistributors()) { - UnifiedPush.saveDistributor(context, context.packageName) - UnifiedPush.registerApp(context) - withContext(Dispatchers.Main) { - onDoneRunnable?.run() - } - return@launch - } - if (force) { - // Un-register first - unregister(pushersManager) - } - // the !force should not be needed - if (!force && UnifiedPush.getDistributor(context).isNotEmpty()) { - UnifiedPush.registerApp(context) - withContext(Dispatchers.Main) { - onDoneRunnable?.run() - } - return@launch - } - - val distributors = UnifiedPush.getDistributors(context) - - if (!force && distributors.size == 1) { - UnifiedPush.saveDistributor(context, distributors.first()) - UnifiedPush.registerApp(context) - withContext(Dispatchers.Main) { - onDoneRunnable?.run() - } - } else { - openDistributorDialogInternal( - activity = activity, - onDoneRunnable = onDoneRunnable, - distributors = distributors - ) - } - } - } - - // TODO remove - // There is no case where this function is called - // with a saved distributor and/or a pusher - private fun openDistributorDialogInternal( - activity: FragmentActivity, - onDoneRunnable: Runnable?, - distributors: List - ) { - val internalDistributorName = stringProvider.getString( - if (fcmHelper.isFirebaseAvailable()) { - R.string.unifiedpush_distributor_fcm_fallback - } else { - R.string.unifiedpush_distributor_background_sync - } - ) - - val distributorsName = distributors.map { - if (it == context.packageName) { - internalDistributorName - } else { - context.getApplicationLabel(it) - } - } - - MaterialAlertDialogBuilder(activity) - .setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title)) - .setItems(distributorsName.toTypedArray()) { _, which -> - val distributor = distributors[which] - - activity.lifecycleScope.launch { - UnifiedPush.saveDistributor(context, distributor) - Timber.i("Saving distributor: $distributor") - UnifiedPush.registerApp(context) - onDoneRunnable?.run() - } - } - .setOnCancelListener { - // By default, use internal solution (fcm/background sync) - UnifiedPush.saveDistributor(context, context.packageName) - UnifiedPush.registerApp(context) - onDoneRunnable?.run() - } - .setCancelable(true) - .show() - } - @MainThread fun showSelectDistributorDialog( context: Context, @@ -217,23 +79,6 @@ class UnifiedPushHelper @Inject constructor( .show() } - // TODO remove - suspend fun unregister(pushersManager: PushersManager? = null) { - val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME - vectorPreferences.setFdroidSyncBackgroundMode(mode) - try { - getEndpointOrToken()?.let { - Timber.d("Removing $it") - pushersManager?.unregisterPusher(it) - } - } catch (e: Exception) { - Timber.d(e, "Probably unregistering a non existing pusher") - } - unifiedPushStore.storeUpEndpoint(null) - unifiedPushStore.storePushGateway(null) - UnifiedPush.unregisterApp(context) - } - @JsonClass(generateAdapter = true) internal data class DiscoveryResponse( @Json(name = "unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush() From aa3a808d2cc670301b5d1920d532b30b85be5b48 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 30 Nov 2022 14:42:56 +0100 Subject: [PATCH 073/197] Do not ask to select push distributor in home if notifications are disabled --- .../im/vector/app/features/home/HomeActivityViewModel.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 7ffc46218c2..cf4bce12f0d 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -116,7 +116,7 @@ class HomeActivityViewModel @AssistedInject constructor( private fun initialize() { if (isInitialized) return isInitialized = true - registerUnifiedPush(distributor = "") + registerUnifiedPushIfNeeded() cleanupFiles() observeInitialSync() checkSessionPushIsOn() @@ -127,6 +127,12 @@ class HomeActivityViewModel @AssistedInject constructor( viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() } } + private fun registerUnifiedPushIfNeeded() { + if(vectorPreferences.areNotificationEnabledForDevice()) { + registerUnifiedPush(distributor = "") + } + } + private fun registerUnifiedPush(distributor: String) { viewModelScope.launch { when (registerUnifiedPushUseCase.execute(distributor = distributor)) { From a3815d70128e35639b2b5b020d96db8927306267 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 30 Nov 2022 15:12:07 +0100 Subject: [PATCH 074/197] Update unit tests --- ...leNotificationsForCurrentSessionUseCase.kt | 1 - ...leNotificationsForCurrentSessionUseCase.kt | 1 - ...tificationsForCurrentSessionUseCaseTest.kt | 12 +-- ...tificationsForCurrentSessionUseCaseTest.kt | 74 ++++++++++++------- .../im/vector/app/test/fakes/FakeFcmHelper.kt | 11 +-- .../app/test/fakes/FakeUnifiedPushHelper.kt | 23 ------ 6 files changed, 56 insertions(+), 66 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt index 2ce2254f2e8..84d92c42914 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt @@ -31,7 +31,6 @@ class DisableNotificationsForCurrentSessionUseCase @Inject constructor( private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, ) { - // TODO update unit tests suspend fun execute() { val session = activeSessionHolder.getSafeActiveSession() ?: return val deviceId = session.sessionParams.deviceId ?: return diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt index e0b0a872f80..99fb249384f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt @@ -37,7 +37,6 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor( object NeedToAskUserForDistributor : EnableNotificationsResult } - // TODO update unit tests suspend fun execute(distributor: String = ""): EnableNotificationsResult { val pusherForCurrentSession = pushersManager.getPusherForCurrentSession() if (pusherForCurrentSession == null) { diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt index e460413a397..386e68ee3a3 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt @@ -16,11 +16,11 @@ package im.vector.app.features.settings.notifications +import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePushersManager -import im.vector.app.test.fakes.FakeUnifiedPushHelper import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.every @@ -33,17 +33,17 @@ private const val A_SESSION_ID = "session-id" class DisableNotificationsForCurrentSessionUseCaseTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() private val fakePushersManager = FakePushersManager() private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk() private val fakeTogglePushNotificationUseCase = mockk() + private val fakeUnregisterUnifiedPushUseCase = mockk() private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase( activeSessionHolder = fakeActiveSessionHolder.instance, - unifiedPushHelper = fakeUnifiedPushHelper.instance, pushersManager = fakePushersManager.instance, checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, togglePushNotificationUseCase = fakeTogglePushNotificationUseCase, + unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase, ) @Test @@ -67,12 +67,14 @@ class DisableNotificationsForCurrentSessionUseCaseTest { val fakeSession = fakeActiveSessionHolder.fakeSession fakeSession.givenSessionId(A_SESSION_ID) every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false - fakeUnifiedPushHelper.givenUnregister(fakePushersManager.instance) + coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) } // When disableNotificationsForCurrentSessionUseCase.execute() // Then - fakeUnifiedPushHelper.verifyUnregister(fakePushersManager.instance) + coVerify { + fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance) + } } } diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt index eb6629cb139..c923f0c7d61 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt @@ -16,18 +16,18 @@ package im.vector.app.features.settings.notifications -import androidx.fragment.app.FragmentActivity -import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase +import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase +import im.vector.app.core.pushers.RegisterUnifiedPushUseCase import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder -import im.vector.app.test.fakes.FakeFcmHelper import im.vector.app.test.fakes.FakePushersManager -import im.vector.app.test.fakes.FakeUnifiedPushHelper import io.mockk.coJustRun -import io.mockk.coVerify import io.mockk.every +import io.mockk.justRun import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe import org.junit.Test private const val A_SESSION_ID = "session-id" @@ -35,53 +35,71 @@ private const val A_SESSION_ID = "session-id" class EnableNotificationsForCurrentSessionUseCaseTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() private val fakePushersManager = FakePushersManager() - private val fakeFcmHelper = FakeFcmHelper() - private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk() private val fakeTogglePushNotificationUseCase = mockk() + private val fakeRegisterUnifiedPushUseCase = mockk() + private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk() private val enableNotificationsForCurrentSessionUseCase = EnableNotificationsForCurrentSessionUseCase( activeSessionHolder = fakeActiveSessionHolder.instance, - unifiedPushHelper = fakeUnifiedPushHelper.instance, pushersManager = fakePushersManager.instance, - fcmHelper = fakeFcmHelper.instance, - checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, togglePushNotificationUseCase = fakeTogglePushNotificationUseCase, + registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase, + ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase, ) @Test - fun `given no existing pusher for current session when execute then a new pusher is registered`() = runTest { + fun `given no existing pusher and a registered distributor when execute then a new pusher is registered and result is success`() = runTest { // Given - val fragmentActivity = mockk() + val aDistributor = "distributor" + val fakeSession = fakeActiveSessionHolder.fakeSession + fakeSession.givenSessionId(A_SESSION_ID) fakePushersManager.givenGetPusherForCurrentSessionReturns(null) - fakeUnifiedPushHelper.givenRegister(fragmentActivity) - fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true) - fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns false + every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success + justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) } + coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) } // When - enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity) + val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor) // Then - fakeUnifiedPushHelper.verifyRegister(fragmentActivity) - fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance, registerPusher = true) + result shouldBe EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Success + verify { + fakeRegisterUnifiedPushUseCase.execute(aDistributor) + fakeEnsureFcmTokenIsRetrievedUseCase.execute(fakePushersManager.instance, registerPusher = true) + } } @Test - fun `given toggle via Pusher is possible when execute then current pusher is toggled to true`() = runTest { + fun `given no existing pusher and a no registered distributor when execute then result is need to ask user for distributor`() = runTest { // Given - val fragmentActivity = mockk() - fakePushersManager.givenGetPusherForCurrentSessionReturns(mockk()) + val aDistributor = "distributor" + fakePushersManager.givenGetPusherForCurrentSessionReturns(null) + every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor + + // When + val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor) + + // Then + result shouldBe EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor + verify { + fakeRegisterUnifiedPushUseCase.execute(aDistributor) + } + } + + @Test + fun `given no deviceId for current session when execute then result is failure`() = runTest { + // Given + val aDistributor = "distributor" val fakeSession = fakeActiveSessionHolder.fakeSession - fakeSession.givenSessionId(A_SESSION_ID) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns true - coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) } + fakeSession.givenSessionId(null) + fakePushersManager.givenGetPusherForCurrentSessionReturns(mockk()) + every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor // When - enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity) + val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor) // Then - coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, true) } + result shouldBe EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt index 11abf187946..07eef36dc12 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt @@ -16,7 +16,6 @@ package im.vector.app.test.fakes -import androidx.fragment.app.FragmentActivity import im.vector.app.core.pushers.FcmHelper import im.vector.app.core.pushers.PushersManager import io.mockk.justRun @@ -27,18 +26,14 @@ class FakeFcmHelper { val instance = mockk() - fun givenEnsureFcmTokenIsRetrieved( - fragmentActivity: FragmentActivity, - pushersManager: PushersManager, - ) { - justRun { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, any()) } + fun givenEnsureFcmTokenIsRetrieved(pushersManager: PushersManager) { + justRun { instance.ensureFcmTokenIsRetrieved(pushersManager, any()) } } fun verifyEnsureFcmTokenIsRetrieved( - fragmentActivity: FragmentActivity, pushersManager: PushersManager, registerPusher: Boolean, ) { - verify { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, registerPusher) } + verify { instance.ensureFcmTokenIsRetrieved(pushersManager, registerPusher) } } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt index 1f2cc8a1ce0..5bc57ead07c 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt @@ -16,37 +16,14 @@ package im.vector.app.test.fakes -import androidx.fragment.app.FragmentActivity -import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnifiedPushHelper -import io.mockk.coJustRun -import io.mockk.coVerify import io.mockk.every import io.mockk.mockk -import io.mockk.verify class FakeUnifiedPushHelper { val instance = mockk() - fun givenRegister(fragmentActivity: FragmentActivity) { - every { instance.register(fragmentActivity, any()) } answers { - secondArg().run() - } - } - - fun verifyRegister(fragmentActivity: FragmentActivity) { - verify { instance.register(fragmentActivity, any()) } - } - - fun givenUnregister(pushersManager: PushersManager) { - coJustRun { instance.unregister(pushersManager) } - } - - fun verifyUnregister(pushersManager: PushersManager) { - coVerify { instance.unregister(pushersManager) } - } - fun givenIsEmbeddedDistributorReturns(isEmbedded: Boolean) { every { instance.isEmbeddedDistributor() } returns isEmbedded } From 46ccf4d73fea7909b64999f7d0786e2684b33e4c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 30 Nov 2022 16:02:55 +0100 Subject: [PATCH 075/197] Adding unit tests for register and unregister use cases --- .../pushers/RegisterUnifiedPushUseCase.kt | 1 - .../pushers/UnregisterUnifiedPushUseCase.kt | 1 - .../pushers/RegisterUnifiedPushUseCaseTest.kt | 158 ++++++++++++++++++ .../UnregisterUnifiedPushUseCaseTest.kt | 83 +++++++++ .../im/vector/app/test/fakes/FakeContext.kt | 4 + .../app/test/fakes/FakePushersManager.kt | 10 ++ .../app/test/fakes/FakeUnifiedPushHelper.kt | 4 + .../app/test/fakes/FakeUnifiedPushStore.kt | 43 +++++ .../app/test/fakes/FakeVectorFeatures.kt | 4 + .../app/test/fakes/FakeVectorPreferences.kt | 9 + 10 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushStore.kt diff --git a/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt index 58bf0f50504..aa3652a54fa 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt @@ -32,7 +32,6 @@ class RegisterUnifiedPushUseCase @Inject constructor( object NeedToAskUserForDistributor : RegisterUnifiedPushResult } - // TODO add unit tests fun execute(distributor: String = ""): RegisterUnifiedPushResult { if (distributor.isNotEmpty()) { saveAndRegisterApp(distributor) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt index 71b1a9c0339..acad3e649f0 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt @@ -31,7 +31,6 @@ class UnregisterUnifiedPushUseCase @Inject constructor( private val unifiedPushHelper: UnifiedPushHelper, ) { - // TODO add unit tests suspend fun execute(pushersManager: PushersManager?) { val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME vectorPreferences.setFdroidSyncBackgroundMode(mode) diff --git a/vector/src/test/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCaseTest.kt new file mode 100644 index 00000000000..c72c519172d --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCaseTest.kt @@ -0,0 +1,158 @@ +/* + * 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.pushers + +import im.vector.app.test.fakes.FakeContext +import im.vector.app.test.fakes.FakeVectorFeatures +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import io.mockk.verifyAll +import io.mockk.verifyOrder +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.unifiedpush.android.connector.UnifiedPush + +class RegisterUnifiedPushUseCaseTest { + + private val fakeContext = FakeContext() + private val fakeVectorFeatures = FakeVectorFeatures() + + private val registerUnifiedPushUseCase = RegisterUnifiedPushUseCase( + context = fakeContext.instance, + vectorFeatures = fakeVectorFeatures, + ) + + @Before + fun setup() { + mockkStatic(UnifiedPush::class) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given non empty distributor when execute then distributor is saved and app is registered`() = runTest { + // Given + val aDistributor = "distributor" + justRun { UnifiedPush.registerApp(any()) } + justRun { UnifiedPush.saveDistributor(any(), any()) } + + // When + val result = registerUnifiedPushUseCase.execute(aDistributor) + + // Then + result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success + verifyOrder { + UnifiedPush.saveDistributor(fakeContext.instance, aDistributor) + UnifiedPush.registerApp(fakeContext.instance) + } + } + + @Test + fun `given external distributors are not allowed when execute then internal distributor is saved and app is registered`() = runTest { + // Given + val aPackageName = "packageName" + fakeContext.givenPackageName(aPackageName) + justRun { UnifiedPush.registerApp(any()) } + justRun { UnifiedPush.saveDistributor(any(), any()) } + fakeVectorFeatures.givenExternalDistributorsAreAllowed(false) + + // When + val result = registerUnifiedPushUseCase.execute() + + // Then + result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success + verifyOrder { + UnifiedPush.saveDistributor(fakeContext.instance, aPackageName) + UnifiedPush.registerApp(fakeContext.instance) + } + } + + @Test + fun `given a saved distributor and external distributors are allowed when execute then app is registered`() = runTest { + // Given + justRun { UnifiedPush.registerApp(any()) } + val aDistributor = "distributor" + every { UnifiedPush.getDistributor(any()) } returns aDistributor + fakeVectorFeatures.givenExternalDistributorsAreAllowed(true) + + // When + val result = registerUnifiedPushUseCase.execute() + + // Then + result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success + verifyAll { + UnifiedPush.getDistributor(fakeContext.instance) + UnifiedPush.registerApp(fakeContext.instance) + } + } + + @Test + fun `given no saved distributor and a unique distributor available when execute then the distributor is saved and app is registered`() = runTest { + // Given + justRun { UnifiedPush.registerApp(any()) } + justRun { UnifiedPush.saveDistributor(any(), any()) } + every { UnifiedPush.getDistributor(any()) } returns "" + fakeVectorFeatures.givenExternalDistributorsAreAllowed(true) + val aDistributor = "distributor" + every { UnifiedPush.getDistributors(any()) } returns listOf(aDistributor) + + // When + val result = registerUnifiedPushUseCase.execute() + + // Then + result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success + verifyOrder { + UnifiedPush.getDistributor(fakeContext.instance) + UnifiedPush.getDistributors(fakeContext.instance) + UnifiedPush.saveDistributor(fakeContext.instance, aDistributor) + UnifiedPush.registerApp(fakeContext.instance) + } + } + + @Test + fun `given no saved distributor and multiple distributors available when execute then result is to ask user`() = runTest { + // Given + every { UnifiedPush.getDistributor(any()) } returns "" + fakeVectorFeatures.givenExternalDistributorsAreAllowed(true) + val aDistributor1 = "distributor1" + val aDistributor2 = "distributor2" + every { UnifiedPush.getDistributors(any()) } returns listOf(aDistributor1, aDistributor2) + + // When + val result = registerUnifiedPushUseCase.execute() + + // Then + result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor + verifyOrder { + UnifiedPush.getDistributor(fakeContext.instance) + UnifiedPush.getDistributors(fakeContext.instance) + } + verify(inverse = true) { + UnifiedPush.saveDistributor(any(), any()) + UnifiedPush.registerApp(any()) + } + } +} diff --git a/vector/src/test/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCaseTest.kt new file mode 100644 index 00000000000..bee545b3e19 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCaseTest.kt @@ -0,0 +1,83 @@ +/* + * 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.pushers + +import im.vector.app.features.settings.BackgroundSyncMode +import im.vector.app.test.fakes.FakeContext +import im.vector.app.test.fakes.FakePushersManager +import im.vector.app.test.fakes.FakeUnifiedPushHelper +import im.vector.app.test.fakes.FakeUnifiedPushStore +import im.vector.app.test.fakes.FakeVectorPreferences +import io.mockk.justRun +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verifyAll +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.unifiedpush.android.connector.UnifiedPush + +class UnregisterUnifiedPushUseCaseTest { + + private val fakeContext = FakeContext() + private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeUnifiedPushStore = FakeUnifiedPushStore() + private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() + + private val unregisterUnifiedPushUseCase = UnregisterUnifiedPushUseCase( + context = fakeContext.instance, + vectorPreferences = fakeVectorPreferences.instance, + unifiedPushStore = fakeUnifiedPushStore.instance, + unifiedPushHelper = fakeUnifiedPushHelper.instance, + ) + + @Before + fun setup() { + mockkStatic(UnifiedPush::class) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given pushersManager when execute then unregister and clean everything which is needed`() = runTest { + // Given + val aEndpoint = "endpoint" + fakeUnifiedPushHelper.givenGetEndpointOrTokenReturns(aEndpoint) + val aPushersManager = FakePushersManager() + aPushersManager.givenUnregisterPusher(aEndpoint) + justRun { UnifiedPush.unregisterApp(any()) } + fakeVectorPreferences.givenSetFdroidSyncBackgroundMode(BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME) + fakeUnifiedPushStore.givenStorePushGateway(null) + fakeUnifiedPushStore.givenStoreUpEndpoint(null) + + // When + unregisterUnifiedPushUseCase.execute(aPushersManager.instance) + + // Then + fakeVectorPreferences.verifySetFdroidSyncBackgroundMode(BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME) + aPushersManager.verifyUnregisterPusher(aEndpoint) + verifyAll { + UnifiedPush.unregisterApp(fakeContext.instance) + } + fakeUnifiedPushStore.verifyStorePushGateway(null) + fakeUnifiedPushStore.verifyStoreUpEndpoint(null) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt index 9a94313fecf..f8c568e9089 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt @@ -81,4 +81,8 @@ class FakeContext( givenService(Context.CLIPBOARD_SERVICE, ClipboardManager::class.java, fakeClipboardManager.instance) return fakeClipboardManager } + + fun givenPackageName(name: String) { + every { instance.packageName } returns name + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt index 46d852f4f87..3dd3854a18d 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt @@ -17,6 +17,8 @@ package im.vector.app.test.fakes import im.vector.app.core.pushers.PushersManager +import io.mockk.coJustRun +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import org.matrix.android.sdk.api.session.pushers.Pusher @@ -28,4 +30,12 @@ class FakePushersManager { fun givenGetPusherForCurrentSessionReturns(pusher: Pusher?) { every { instance.getPusherForCurrentSession() } returns pusher } + + fun givenUnregisterPusher(pushKey: String) { + coJustRun { instance.unregisterPusher(pushKey) } + } + + fun verifyUnregisterPusher(pushKey: String) { + coVerify { instance.unregisterPusher(pushKey) } + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt index 5bc57ead07c..99b5b758742 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt @@ -27,4 +27,8 @@ class FakeUnifiedPushHelper { fun givenIsEmbeddedDistributorReturns(isEmbedded: Boolean) { every { instance.isEmbeddedDistributor() } returns isEmbedded } + + fun givenGetEndpointOrTokenReturns(endpoint: String?) { + every { instance.getEndpointOrToken() } returns endpoint + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushStore.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushStore.kt new file mode 100644 index 00000000000..9b09bec6882 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushStore.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import im.vector.app.core.pushers.UnifiedPushStore +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify + +class FakeUnifiedPushStore { + + val instance = mockk() + + fun givenStoreUpEndpoint(endpoint: String?) { + justRun { instance.storeUpEndpoint(endpoint) } + } + + fun verifyStoreUpEndpoint(endpoint: String?) { + verify { instance.storeUpEndpoint(endpoint) } + } + + fun givenStorePushGateway(gateway: String?) { + justRun { instance.storePushGateway(gateway) } + } + + fun verifyStorePushGateway(gateway: String?) { + verify { instance.storePushGateway(gateway) } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt index c3c2fa684f8..b399f0baa41 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt @@ -54,4 +54,8 @@ class FakeVectorFeatures : VectorFeatures by spyk() { fun givenUnverifiedSessionsAlertEnabled(isEnabled: Boolean) { every { isUnverifiedSessionsAlertEnabled() } returns isEnabled } + + fun givenExternalDistributorsAreAllowed(allowed: Boolean) { + every { allowExternalUnifiedPushDistributors() } returns allowed + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index 77df3ffc7ac..7970c14e90e 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -16,6 +16,7 @@ package im.vector.app.test.fakes +import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences import io.mockk.every import io.mockk.justRun @@ -60,4 +61,12 @@ class FakeVectorPreferences { fun givenUnverifiedSessionsAlertLastShownMillis(lastShownMillis: Long) { every { instance.getUnverifiedSessionsAlertLastShownMillis(any()) } returns lastShownMillis } + + fun givenSetFdroidSyncBackgroundMode(mode: BackgroundSyncMode) { + justRun { instance.setFdroidSyncBackgroundMode(mode) } + } + + fun verifySetFdroidSyncBackgroundMode(mode: BackgroundSyncMode) { + verify { instance.setFdroidSyncBackgroundMode(mode) } + } } From 2a8c72bdcf60409b78e51fad4af53c49bcacc5d4 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 30 Nov 2022 16:05:25 +0100 Subject: [PATCH 076/197] Fixing code style issues --- .../java/im/vector/app/core/di/MavericksViewModelModule.kt | 4 +++- vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt | 1 - .../java/im/vector/app/features/home/HomeActivityViewModel.kt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index ad3e3617750..b58d584dad5 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -688,5 +688,7 @@ interface MavericksViewModelModule { @Binds @IntoMap @MavericksViewModelKey(VectorSettingsNotificationPreferenceViewModel::class) - fun vectorSettingsNotificationPreferenceViewModelFactory(factory: VectorSettingsNotificationPreferenceViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + fun vectorSettingsNotificationPreferenceViewModelFactory( + factory: VectorSettingsNotificationPreferenceViewModel.Factory + ): MavericksAssistedViewModelFactory<*, *> } diff --git a/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt index 0cc251ce31e..381348638d6 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt @@ -16,7 +16,6 @@ package im.vector.app.core.pushers -import android.app.Activity import im.vector.app.core.di.ActiveSessionHolder interface FcmHelper { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index cf4bce12f0d..26034fc09cb 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -128,7 +128,7 @@ class HomeActivityViewModel @AssistedInject constructor( } private fun registerUnifiedPushIfNeeded() { - if(vectorPreferences.areNotificationEnabledForDevice()) { + if (vectorPreferences.areNotificationEnabledForDevice()) { registerUnifiedPush(distributor = "") } } From e78e19285344a9b68db8542226dcacac56c110de Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 30 Nov 2022 16:37:15 +0100 Subject: [PATCH 077/197] Adding unit tests for FCM token retrieval --- .../EnsureFcmTokenIsRetrievedUseCase.kt | 1 - .../EnsureFcmTokenIsRetrievedUseCaseTest.kt | 106 ++++++++++++++++++ .../im/vector/app/test/fakes/FakeFcmHelper.kt | 3 +- 3 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt index e55d0426ba1..cb955e01f77 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt @@ -25,7 +25,6 @@ class EnsureFcmTokenIsRetrievedUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, ) { - // TODO add unit tests fun execute(pushersManager: PushersManager, registerPusher: Boolean) { if (unifiedPushHelper.isEmbeddedDistributor()) { fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher)) diff --git a/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt new file mode 100644 index 00000000000..03a43a5b551 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt @@ -0,0 +1,106 @@ +/* + * 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.pushers + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeFcmHelper +import im.vector.app.test.fakes.FakePushersManager +import im.vector.app.test.fakes.FakeUnifiedPushHelper +import im.vector.app.test.fixtures.PusherFixture +import io.mockk.verify +import org.junit.Test + +class EnsureFcmTokenIsRetrievedUseCaseTest { + + private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() + private val fakeFcmHelper = FakeFcmHelper() + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val ensureFcmTokenIsRetrievedUseCase = EnsureFcmTokenIsRetrievedUseCase( + unifiedPushHelper = fakeUnifiedPushHelper.instance, + fcmHelper = fakeFcmHelper.instance, + activeSessionHolder = fakeActiveSessionHolder.instance, + ) + + @Test + fun `given no registered pusher and distributor as embedded when execute then ensure the FCM token is retrieved with register pusher option`() { + // Given + val aPushersManager = FakePushersManager() + fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true) + fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(aPushersManager.instance) + val aSessionId = "aSessionId" + fakeActiveSessionHolder.fakeSession.givenSessionId(aSessionId) + val expectedPusher = PusherFixture.aPusher(deviceId = "") + fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(expectedPusher)) + + // When + ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = true) + + // Then + fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = true) + } + + @Test + fun `given a registered pusher and distributor as embedded when execute then ensure the FCM token is retrieved without register pusher option`() { + // Given + val aPushersManager = FakePushersManager() + fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true) + fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(aPushersManager.instance) + val aSessionId = "aSessionId" + fakeActiveSessionHolder.fakeSession.givenSessionId(aSessionId) + val expectedPusher = PusherFixture.aPusher(deviceId = aSessionId) + fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(expectedPusher)) + + // When + ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = true) + + // Then + fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = false) + } + + @Test + fun `given no registering asked and distributor as embedded when execute then ensure the FCM token is retrieved without register pusher option`() { + // Given + val aPushersManager = FakePushersManager() + fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true) + fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(aPushersManager.instance) + val aSessionId = "aSessionId" + fakeActiveSessionHolder.fakeSession.givenSessionId(aSessionId) + val expectedPusher = PusherFixture.aPusher(deviceId = aSessionId) + fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(expectedPusher)) + + // When + ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = false) + + // Then + fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = false) + } + + @Test + fun `given distributor as not embedded when execute then nothing is done`() { + // Given + val aPushersManager = FakePushersManager() + fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(false) + + // When + ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = true) + + // Then + fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = true, inverse = true) + fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = false, inverse = true) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt index 07eef36dc12..4c210215ecf 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt @@ -33,7 +33,8 @@ class FakeFcmHelper { fun verifyEnsureFcmTokenIsRetrieved( pushersManager: PushersManager, registerPusher: Boolean, + inverse: Boolean = false, ) { - verify { instance.ensureFcmTokenIsRetrieved(pushersManager, registerPusher) } + verify(inverse = inverse) { instance.ensureFcmTokenIsRetrieved(pushersManager, registerPusher) } } } From d31652e91007e67f6ea5a8065c6f032af9607e6f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 30 Nov 2022 17:21:48 +0100 Subject: [PATCH 078/197] Adding unit tests for settings ViewModel --- ...SettingsNotificationPreferenceViewModel.kt | 1 - ...ingsNotificationPreferenceViewModelTest.kt | 202 ++++++++++++++++++ .../app/test/fakes/FakeVectorPreferences.kt | 4 + 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt index 59c26749c9c..d6a9c621f2b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt @@ -49,7 +49,6 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() - // TODO add unit tests override fun handle(action: VectorSettingsNotificationPreferenceViewAction) { when (action) { VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice -> handleDisableNotificationsForDevice() diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt new file mode 100644 index 00000000000..f9d75273164 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.notifications + +import com.airbnb.mvrx.test.MavericksTestRule +import im.vector.app.core.platform.VectorDummyViewState +import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase +import im.vector.app.core.pushers.RegisterUnifiedPushUseCase +import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase +import im.vector.app.test.fakes.FakePushersManager +import im.vector.app.test.fakes.FakeVectorPreferences +import im.vector.app.test.test +import im.vector.app.test.testDispatcher +import io.mockk.coEvery +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.justRun +import io.mockk.mockk +import org.junit.Rule +import org.junit.Test + +class VectorSettingsNotificationPreferenceViewModelTest { + + @get:Rule + val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher) + + private val fakePushersManager = FakePushersManager() + private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeEnableNotificationsForCurrentSessionUseCase = mockk() + private val fakeDisableNotificationsForCurrentSessionUseCase = mockk() + private val fakeUnregisterUnifiedPushUseCase = mockk() + private val fakeRegisterUnifiedPushUseCase = mockk() + private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk() + + private fun createViewModel() = VectorSettingsNotificationPreferenceViewModel( + initialState = VectorDummyViewState(), + pushersManager = fakePushersManager.instance, + vectorPreferences = fakeVectorPreferences.instance, + enableNotificationsForCurrentSessionUseCase = fakeEnableNotificationsForCurrentSessionUseCase, + disableNotificationsForCurrentSessionUseCase = fakeDisableNotificationsForCurrentSessionUseCase, + unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase, + registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase, + ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase, + ) + + @Test + fun `given DisableNotificationsForDevice action when handling action then disable use case is called`() { + // Given + val viewModel = createViewModel() + val action = VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice + coJustRun { fakeDisableNotificationsForCurrentSessionUseCase.execute() } + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled + + // When + val viewModelTest = viewModel.test() + viewModel.handle(action) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + coVerify { + fakeDisableNotificationsForCurrentSessionUseCase.execute() + } + } + + @Test + fun `given EnableNotificationsForDevice action and enable success when handling action then enable use case is called`() { + // Given + val viewModel = createViewModel() + val aDistributor = "aDistributor" + val action = VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(aDistributor) + coEvery { fakeEnableNotificationsForCurrentSessionUseCase.execute(any()) } returns + EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Success + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled + + // When + val viewModelTest = viewModel.test() + viewModel.handle(action) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + coVerify { + fakeEnableNotificationsForCurrentSessionUseCase.execute(aDistributor) + } + } + + @Test + fun `given EnableNotificationsForDevice action and enable needs user choice when handling action then enable use case is called`() { + // Given + val viewModel = createViewModel() + val aDistributor = "aDistributor" + val action = VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(aDistributor) + coEvery { fakeEnableNotificationsForCurrentSessionUseCase.execute(any()) } returns + EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor + + // When + val viewModelTest = viewModel.test() + viewModel.handle(action) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + coVerify { + fakeEnableNotificationsForCurrentSessionUseCase.execute(aDistributor) + } + } + + @Test + fun `given EnableNotificationsForDevice action and enable failure when handling action then enable use case is called`() { + // Given + val viewModel = createViewModel() + val aDistributor = "aDistributor" + val action = VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(aDistributor) + coEvery { fakeEnableNotificationsForCurrentSessionUseCase.execute(any()) } returns + EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure + + // When + val viewModelTest = viewModel.test() + viewModel.handle(action) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + coVerify { + fakeEnableNotificationsForCurrentSessionUseCase.execute(aDistributor) + } + } + + @Test + fun `given RegisterPushDistributor action and register success when handling action then register use case is called`() { + // Given + val viewModel = createViewModel() + val aDistributor = "aDistributor" + val action = VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(aDistributor) + coEvery { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success + coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) } + val areNotificationsEnabled = true + fakeVectorPreferences.givenAreNotificationsEnabledForDevice(areNotificationsEnabled) + justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) } + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged + + // When + val viewModelTest = viewModel.test() + viewModel.handle(action) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + coVerifyOrder { + fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance) + fakeRegisterUnifiedPushUseCase.execute(aDistributor) + fakeEnsureFcmTokenIsRetrievedUseCase.execute(fakePushersManager.instance, registerPusher = areNotificationsEnabled) + } + } + + @Test + fun `given RegisterPushDistributor action and register needs user choice when handling action then register use case is called`() { + // Given + val viewModel = createViewModel() + val aDistributor = "aDistributor" + val action = VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(aDistributor) + coEvery { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor + coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) } + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor + + // When + val viewModelTest = viewModel.test() + viewModel.handle(action) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + coVerifyOrder { + fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance) + fakeRegisterUnifiedPushUseCase.execute(aDistributor) + } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index 7970c14e90e..06efca1bf77 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -69,4 +69,8 @@ class FakeVectorPreferences { fun verifySetFdroidSyncBackgroundMode(mode: BackgroundSyncMode) { verify { instance.setFdroidSyncBackgroundMode(mode) } } + + fun givenAreNotificationsEnabledForDevice(notificationsEnabled: Boolean) { + every { instance.areNotificationEnabledForDevice() } returns notificationsEnabled + } } From f8c59f6b0c82608484463d1f3157c1203102bccc Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 1 Dec 2022 10:22:09 +0100 Subject: [PATCH 079/197] Removing unused import --- .../app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt index 03a43a5b551..fca49adc9bb 100644 --- a/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt @@ -21,7 +21,6 @@ import im.vector.app.test.fakes.FakeFcmHelper import im.vector.app.test.fakes.FakePushersManager import im.vector.app.test.fakes.FakeUnifiedPushHelper import im.vector.app.test.fixtures.PusherFixture -import io.mockk.verify import org.junit.Test class EnsureFcmTokenIsRetrievedUseCaseTest { From 0c6781e9ef1ca7784539f645a8ce7b2b508c918e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 16 Nov 2022 14:37:48 +0100 Subject: [PATCH 080/197] Adding changelog entry --- changelog.d/7596.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7596.feature diff --git a/changelog.d/7596.feature b/changelog.d/7596.feature new file mode 100644 index 00000000000..022d86342b5 --- /dev/null +++ b/changelog.d/7596.feature @@ -0,0 +1 @@ +Save m.local_notification_settings. event in account_data From 9d684bc021b5af66b8309d24dd398f61bdbad7f6 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 16 Nov 2022 16:31:00 +0100 Subject: [PATCH 081/197] Check if account data has content to decide if push notifications can be toggled using account data --- ...ePushNotificationsViaAccountDataUseCase.kt | 7 +++++- ...hNotificationsViaAccountDataUseCaseTest.kt | 23 +++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt index 194a2aebbf3..a1b87bc3960 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt @@ -16,8 +16,10 @@ package im.vector.app.features.settings.devices.v2.notification +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toModel import javax.inject.Inject class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() { @@ -25,6 +27,9 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor fun execute(session: Session, deviceId: String): Boolean { return session .accountDataService() - .getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null + .getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) + ?.content + .toModel() + ?.isSilenced != null } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt index 37433364e88..94f142cbe64 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt @@ -20,7 +20,9 @@ import im.vector.app.test.fakes.FakeSession import io.mockk.mockk import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toContent private const val A_DEVICE_ID = "device-id" @@ -32,13 +34,13 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { CheckIfCanTogglePushNotificationsViaAccountDataUseCase() @Test - fun `given current session and an account data for the device id when execute then result is true`() { + fun `given current session and an account data with a content for the device id when execute then result is true`() { // Given fakeSession .accountDataService() .givenGetUserAccountDataEventReturns( type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, - content = mockk(), + content = LocalNotificationSettingsContent(isSilenced = true).toContent(), ) // When @@ -48,6 +50,23 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { result shouldBeEqualTo true } + @Test + fun `given current session and an account data with empty content for the device id when execute then result is false`() { + // Given + fakeSession + .accountDataService() + .givenGetUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, + content = mockk(), + ) + + // When + val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) + + // Then + result shouldBeEqualTo false + } + @Test fun `given current session and NO account data for the device id when execute then result is false`() { // Given From c56eb331db5cfe652d70ba4d3f293dbb118dd55c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 16 Nov 2022 16:38:46 +0100 Subject: [PATCH 082/197] Update use cases to enable/disable push notifications for the current session --- .../DisableNotificationsForCurrentSessionUseCase.kt | 5 ++--- .../DisableNotificationsForCurrentSessionUseCaseTest.kt | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt index 84d92c42914..4d890ca678d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt @@ -34,9 +34,8 @@ class DisableNotificationsForCurrentSessionUseCase @Inject constructor( suspend fun execute() { val session = activeSessionHolder.getSafeActiveSession() ?: return val deviceId = session.sessionParams.deviceId ?: return - if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { - togglePushNotificationUseCase.execute(deviceId, enabled = false) - } else { + togglePushNotificationUseCase.execute(deviceId, enabled = false) + if (!checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { unregisterUnifiedPushUseCase.execute(pushersManager) } } diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt index 386e68ee3a3..a84aa4b055a 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt @@ -67,6 +67,7 @@ class DisableNotificationsForCurrentSessionUseCaseTest { val fakeSession = fakeActiveSessionHolder.fakeSession fakeSession.givenSessionId(A_SESSION_ID) every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false + coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) } coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) } // When @@ -74,6 +75,7 @@ class DisableNotificationsForCurrentSessionUseCaseTest { // Then coVerify { + fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, false) fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance) } } From 81c64503f2f5956c3ef331128b251538c16c9b22 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 16 Nov 2022 18:02:59 +0100 Subject: [PATCH 083/197] Adding SetNotificationSettingsAccountDataUseCase --- ...tNotificationSettingsAccountDataUseCase.kt | 36 +++++++++++++ ...ificationSettingsAccountDataUseCaseTest.kt | 51 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt new file mode 100644 index 00000000000..f0ec9d5ddcc --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toContent +import javax.inject.Inject + +class SetNotificationSettingsAccountDataUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + suspend fun execute(deviceId: String, localNotificationSettingsContent: LocalNotificationSettingsContent) { + val session = activeSessionHolder.getSafeActiveSession() ?: return + session.accountDataService().updateUserAccountData( + UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId, + localNotificationSettingsContent.toContent(), + ) + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt new file mode 100644 index 00000000000..8f72f0946f5 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.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.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toContent + +class SetNotificationSettingsAccountDataUseCaseTest { + + private val activeSessionHolder = FakeActiveSessionHolder() + + private val setNotificationSettingsAccountDataUseCase = SetNotificationSettingsAccountDataUseCase( + activeSessionHolder = activeSessionHolder.instance, + ) + + @Test + fun `given a content when execute then update local notification settings with this content`() = runTest { + // Given + val sessionId = "a_session_id" + val localNotificationSettingsContent = LocalNotificationSettingsContent() + val fakeSession = activeSessionHolder.fakeSession + fakeSession.accountDataService().givenUpdateUserAccountDataEventSucceeds() + + // When + setNotificationSettingsAccountDataUseCase.execute(sessionId, localNotificationSettingsContent) + + // Then + activeSessionHolder.fakeSession.accountDataService().verifyUpdateUserAccountDataEventSucceeds( + UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, + localNotificationSettingsContent.toContent(), + ) + } +} From b163b42d3d54f7c296d723b491b2e7d067f71ff5 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 17 Nov 2022 10:14:51 +0100 Subject: [PATCH 084/197] Use new sub usecase in the TogglePushNotificationUseCase --- .../TogglePushNotificationUseCase.kt | 9 +++---- .../TogglePushNotificationUseCaseTest.kt | 27 +++++++++---------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt index 7969bbbe9bd..b8a6d7343bc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt @@ -18,14 +18,14 @@ package im.vector.app.features.settings.devices.v2.notification import im.vector.app.core.di.ActiveSessionHolder import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes -import org.matrix.android.sdk.api.session.events.model.toContent import javax.inject.Inject +// TODO rename into ToggleNotificationsUseCase class TogglePushNotificationUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, + private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase, ) { suspend fun execute(deviceId: String, enabled: Boolean) { @@ -40,10 +40,7 @@ class TogglePushNotificationUseCase @Inject constructor( if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId)) { val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled) - session.accountDataService().updateUserAccountData( - UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId, - newNotificationSettingsContent.toContent(), - ) + setNotificationSettingsAccountDataUseCase.execute(deviceId, newNotificationSettingsContent) } } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt index 35c5979e538..e443dc3e942 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt @@ -18,13 +18,13 @@ package im.vector.app.features.settings.devices.v2.notification import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fixtures.PusherFixture +import io.mockk.coJustRun +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes -import org.matrix.android.sdk.api.session.events.model.toContent class TogglePushNotificationUseCaseTest { @@ -33,12 +33,15 @@ class TogglePushNotificationUseCaseTest { mockk() private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase = mockk() + private val fakeSetNotificationSettingsAccountDataUseCase = + mockk() private val togglePushNotificationUseCase = TogglePushNotificationUseCase( activeSessionHolder = activeSessionHolder.instance, checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase, + setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, ) @Test @@ -66,26 +69,20 @@ class TogglePushNotificationUseCaseTest { fun `when execute, then toggle local notification settings`() = runTest { // Given val sessionId = "a_session_id" - val pushers = listOf( - PusherFixture.aPusher(deviceId = sessionId, enabled = false), - PusherFixture.aPusher(deviceId = "another id", enabled = false) - ) val fakeSession = activeSessionHolder.fakeSession - fakeSession.pushersService().givenPushersLive(pushers) - fakeSession.accountDataService().givenGetUserAccountDataEventReturns( - UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, - LocalNotificationSettingsContent(isSilenced = true).toContent() - ) every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true + coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any()) } + val expectedLocalNotificationSettingsContent = LocalNotificationSettingsContent( + isSilenced = false + ) // When togglePushNotificationUseCase.execute(sessionId, true) // Then - activeSessionHolder.fakeSession.accountDataService().verifyUpdateUserAccountDataEventSucceeds( - UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, - LocalNotificationSettingsContent(isSilenced = false).toContent(), - ) + coVerify { + fakeSetNotificationSettingsAccountDataUseCase.execute(sessionId, expectedLocalNotificationSettingsContent) + } } } From 14b21dc0397c5b4e77a5ad5c94e220847b82cb14 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 17 Nov 2022 10:34:11 +0100 Subject: [PATCH 085/197] Adding use cases to create and delete notifications settings in account data --- .../LocalNotificationSettingsContent.kt | 3 +- ...eNotificationSettingsAccountDataUseCase.kt | 38 ++++++++++++ ...eNotificationSettingsAccountDataUseCase.kt | 33 +++++++++++ ...ificationSettingsAccountDataUseCaseTest.kt | 59 +++++++++++++++++++ ...ificationSettingsAccountDataUseCaseTest.kt | 49 +++++++++++++++ 5 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt index 2a95ccce7a7..6998d9dcf27 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt @@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class LocalNotificationSettingsContent( - @Json(name = "is_silenced") val isSilenced: Boolean = false + @Json(name = "is_silenced") + val isSilenced: Boolean? = false ) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCase.kt new file mode 100644 index 00000000000..e2ee19e5cdc --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCase.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import im.vector.app.features.settings.VectorPreferences +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject + +class CreateNotificationSettingsAccountDataUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, + private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase +) { + + // TODO to be called on session start when background sync is enabled + when switching to background sync + suspend fun execute(session: Session) { + val deviceId = session.sessionParams.deviceId ?: return + val isSilenced = !vectorPreferences.areNotificationEnabledForDevice() + val notificationSettingsContent = LocalNotificationSettingsContent( + isSilenced = isSilenced + ) + setNotificationSettingsAccountDataUseCase.execute(deviceId, notificationSettingsContent) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt new file mode 100644 index 00000000000..51c24e500c1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import javax.inject.Inject + +class DeleteNotificationSettingsAccountDataUseCase @Inject constructor( + private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase, +) { + + // TODO to be called when switching to push notifications method + suspend fun execute(deviceId: String) { + val emptyNotificationSettingsContent = LocalNotificationSettingsContent( + isSilenced = null + ) + setNotificationSettingsAccountDataUseCase.execute(deviceId, emptyNotificationSettingsContent) + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt new file mode 100644 index 00000000000..e4cadaa0052 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.FakeVectorPreferences +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent + +class CreateNotificationSettingsAccountDataUseCaseTest { + + private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeSetNotificationSettingsAccountDataUseCase = mockk() + + private val createNotificationSettingsAccountDataUseCase = CreateNotificationSettingsAccountDataUseCase( + vectorPreferences = fakeVectorPreferences.instance, + setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, + ) + + @Test + fun `given a device id when execute then content with the current notification preference is set for the account data`() = runTest { + // Given + val aDeviceId = "device-id" + val session = FakeSession() + session.givenSessionId(aDeviceId) + coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any()) } + val areNotificationsEnabled = true + fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled) + val expectedContent = LocalNotificationSettingsContent( + isSilenced = !areNotificationsEnabled + ) + + // When + createNotificationSettingsAccountDataUseCase.execute(session) + + // Then + verify { fakeVectorPreferences.instance.areNotificationEnabledForDevice() } + coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aDeviceId, expectedContent) } + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt new file mode 100644 index 00000000000..038a1f436a0 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent + +class DeleteNotificationSettingsAccountDataUseCaseTest { + + private val fakeSetNotificationSettingsAccountDataUseCase = mockk() + + private val deleteNotificationSettingsAccountDataUseCase = DeleteNotificationSettingsAccountDataUseCase( + setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, + ) + + @Test + fun `given a device id when execute then empty content is set for the account data`() = runTest { + // Given + val aDeviceId = "device-id" + coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any()) } + val expectedContent = LocalNotificationSettingsContent( + isSilenced = null + ) + + // When + deleteNotificationSettingsAccountDataUseCase.execute(aDeviceId) + + // Then + coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aDeviceId, expectedContent) } + } +} From 7c51174d7ef3940cbb550dcbd89dcce942ae4bec Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 17 Nov 2022 11:04:21 +0100 Subject: [PATCH 086/197] Renaming some use cases to be consistent --- ...CanToggleNotificationsViaPusherUseCase.kt} | 2 +- ...ggleNotificationsViaAccountDataUseCase.kt} | 2 +- ...CanToggleNotificationsViaPusherUseCase.kt} | 2 +- .../GetNotificationsStatusUseCase.kt | 8 ++--- ...seCase.kt => ToggleNotificationUseCase.kt} | 11 ++++--- .../v2/overview/SessionOverviewViewModel.kt | 6 ++-- ...leNotificationsForCurrentSessionUseCase.kt | 12 ++++---- ...leNotificationsForCurrentSessionUseCase.kt | 6 ++-- ...oggleNotificationsViaPusherUseCaseTest.kt} | 8 ++--- ...NotificationsViaAccountDataUseCaseTest.kt} | 12 ++++---- ...oggleNotificationsViaPusherUseCaseTest.kt} | 8 ++--- .../GetNotificationsStatusUseCaseTest.kt | 28 ++++++++--------- ...st.kt => ToggleNotificationUseCaseTest.kt} | 30 +++++++++---------- ...tificationsForCurrentSessionUseCaseTest.kt | 22 +++++++------- ...tificationsForCurrentSessionUseCaseTest.kt | 8 ++--- .../FakeTogglePushNotificationUseCase.kt | 4 +-- 16 files changed, 85 insertions(+), 84 deletions(-) rename vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/{CanTogglePushNotificationsViaPusherUseCase.kt => CanToggleNotificationsViaPusherUseCase.kt} (94%) rename vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/{CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt => CheckIfCanToggleNotificationsViaAccountDataUseCase.kt} (93%) rename vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/{CheckIfCanTogglePushNotificationsViaPusherUseCase.kt => CheckIfCanToggleNotificationsViaPusherUseCase.kt} (92%) rename vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/{TogglePushNotificationUseCase.kt => ToggleNotificationUseCase.kt} (74%) rename vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/{CanTogglePushNotificationsViaPusherUseCaseTest.kt => CanToggleNotificationsViaPusherUseCaseTest.kt} (87%) rename vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/{CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt => CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt} (83%) rename vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/{CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt => CheckIfCanToggleNotificationsViaPusherUseCaseTest.kt} (82%) rename vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/{TogglePushNotificationUseCaseTest.kt => ToggleNotificationUseCaseTest.kt} (70%) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCase.kt similarity index 94% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCase.kt index 0125d92ba60..96521ec78ce 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCase.kt @@ -24,7 +24,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.flow.unwrap import javax.inject.Inject -class CanTogglePushNotificationsViaPusherUseCase @Inject constructor() { +class CanToggleNotificationsViaPusherUseCase @Inject constructor() { fun execute(session: Session): Flow { return session diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt similarity index 93% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt index a1b87bc3960..b006e3da452 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.toModel import javax.inject.Inject -class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() { +class CheckIfCanToggleNotificationsViaAccountDataUseCase @Inject constructor() { fun execute(session: Session, deviceId: String): Boolean { return session diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCase.kt similarity index 92% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCase.kt index ca314bf145b..1dc186be7cd 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCase.kt @@ -19,7 +19,7 @@ package im.vector.app.features.settings.devices.v2.notification import org.matrix.android.sdk.api.session.Session import javax.inject.Inject -class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor() { +class CheckIfCanToggleNotificationsViaPusherUseCase @Inject constructor() { fun execute(session: Session): Boolean { return session diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 03e4e31f2ec..f98fd63efba 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -30,13 +30,13 @@ import org.matrix.android.sdk.flow.unwrap import javax.inject.Inject class GetNotificationsStatusUseCase @Inject constructor( - private val canTogglePushNotificationsViaPusherUseCase: CanTogglePushNotificationsViaPusherUseCase, - private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, + private val canToggleNotificationsViaPusherUseCase: CanToggleNotificationsViaPusherUseCase, + private val checkIfCanToggleNotificationsViaAccountDataUseCase: CheckIfCanToggleNotificationsViaAccountDataUseCase, ) { fun execute(session: Session, deviceId: String): Flow { return when { - checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId) -> { + checkIfCanToggleNotificationsViaAccountDataUseCase.execute(session, deviceId) -> { session.flow() .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) .unwrap() @@ -44,7 +44,7 @@ class GetNotificationsStatusUseCase @Inject constructor( .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } .distinctUntilChanged() } - else -> canTogglePushNotificationsViaPusherUseCase.execute(session) + else -> canToggleNotificationsViaPusherUseCase.execute(session) .flatMapLatest { canToggle -> if (canToggle) { session.flow() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt similarity index 74% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt index b8a6d7343bc..d0e1ea2a7a4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt @@ -20,25 +20,24 @@ import im.vector.app.core.di.ActiveSessionHolder import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent import javax.inject.Inject -// TODO rename into ToggleNotificationsUseCase -class TogglePushNotificationUseCase @Inject constructor( +class ToggleNotificationUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, - private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, - private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, + private val checkIfCanToggleNotificationsViaPusherUseCase: CheckIfCanToggleNotificationsViaPusherUseCase, + private val checkIfCanToggleNotificationsViaAccountDataUseCase: CheckIfCanToggleNotificationsViaAccountDataUseCase, private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase, ) { suspend fun execute(deviceId: String, enabled: Boolean) { val session = activeSessionHolder.getSafeActiveSession() ?: return - if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { + if (checkIfCanToggleNotificationsViaPusherUseCase.execute(session)) { val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } devicePusher?.let { pusher -> session.pushersService().togglePusher(pusher, enabled) } } - if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId)) { + if (checkIfCanToggleNotificationsViaAccountDataUseCase.execute(session, deviceId)) { val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled) setNotificationSettingsAccountDataUseCase.execute(deviceId, newNotificationSettingsContent) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 472e0a42690..0ddf688514e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -31,7 +31,7 @@ import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase -import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase @@ -54,7 +54,7 @@ class SessionOverviewViewModel @AssistedInject constructor( private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, private val activeSessionHolder: ActiveSessionHolder, - private val togglePushNotificationUseCase: TogglePushNotificationUseCase, + private val toggleNotificationUseCase: ToggleNotificationUseCase, private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase, refreshDevicesUseCase: RefreshDevicesUseCase, private val vectorPreferences: VectorPreferences, @@ -228,7 +228,7 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) { viewModelScope.launch { - togglePushNotificationUseCase.execute(action.deviceId, action.enabled) + toggleNotificationUseCase.execute(action.deviceId, action.enabled) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt index 4d890ca678d..380c3b5e4e4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt @@ -19,23 +19,23 @@ package im.vector.app.features.settings.notifications import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase -import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase -import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase +import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase import javax.inject.Inject class DisableNotificationsForCurrentSessionUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, private val pushersManager: PushersManager, - private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, - private val togglePushNotificationUseCase: TogglePushNotificationUseCase, + private val checkIfCanToggleNotificationsViaPusherUseCase: CheckIfCanToggleNotificationsViaPusherUseCase, + private val toggleNotificationUseCase: ToggleNotificationUseCase, private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, ) { suspend fun execute() { val session = activeSessionHolder.getSafeActiveSession() ?: return val deviceId = session.sessionParams.deviceId ?: return - togglePushNotificationUseCase.execute(deviceId, enabled = false) - if (!checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { + toggleNotificationUseCase.execute(deviceId, enabled = false) + if (!checkIfCanToggleNotificationsViaPusherUseCase.execute(session)) { unregisterUnifiedPushUseCase.execute(pushersManager) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt index 99fb249384f..89633a10c24 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt @@ -20,13 +20,13 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.RegisterUnifiedPushUseCase -import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase import javax.inject.Inject class EnableNotificationsForCurrentSessionUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, private val pushersManager: PushersManager, - private val togglePushNotificationUseCase: TogglePushNotificationUseCase, + private val toggleNotificationUseCase: ToggleNotificationUseCase, private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase, ) { @@ -52,7 +52,7 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor( val session = activeSessionHolder.getSafeActiveSession() ?: return EnableNotificationsResult.Failure val deviceId = session.sessionParams.deviceId ?: return EnableNotificationsResult.Failure - togglePushNotificationUseCase.execute(deviceId, enabled = true) + toggleNotificationUseCase.execute(deviceId, enabled = true) return EnableNotificationsResult.Success } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCaseTest.kt similarity index 87% rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCaseTest.kt index 997fa827f53..3284adb32d7 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCaseTest.kt @@ -30,13 +30,13 @@ import org.junit.Test private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyTogglePushNotificationsOfDevices = true) -class CanTogglePushNotificationsViaPusherUseCaseTest { +class CanToggleNotificationsViaPusherUseCaseTest { private val fakeSession = FakeSession() private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions() - private val canTogglePushNotificationsViaPusherUseCase = - CanTogglePushNotificationsViaPusherUseCase() + private val canToggleNotificationsViaPusherUseCase = + CanToggleNotificationsViaPusherUseCase() @Before fun setUp() { @@ -57,7 +57,7 @@ class CanTogglePushNotificationsViaPusherUseCaseTest { .givenAsFlow() // When - val result = canTogglePushNotificationsViaPusherUseCase.execute(fakeSession).firstOrNull() + val result = canToggleNotificationsViaPusherUseCase.execute(fakeSession).firstOrNull() // Then result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt similarity index 83% rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt index 94f142cbe64..de225d36ac6 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt @@ -26,12 +26,12 @@ import org.matrix.android.sdk.api.session.events.model.toContent private const val A_DEVICE_ID = "device-id" -class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { +class CheckIfCanToggleNotificationsViaAccountDataUseCaseTest { private val fakeSession = FakeSession() - private val checkIfCanTogglePushNotificationsViaAccountDataUseCase = - CheckIfCanTogglePushNotificationsViaAccountDataUseCase() + private val checkIfCanToggleNotificationsViaAccountDataUseCase = + CheckIfCanToggleNotificationsViaAccountDataUseCase() @Test fun `given current session and an account data with a content for the device id when execute then result is true`() { @@ -44,7 +44,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { ) // When - val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) + val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) // Then result shouldBeEqualTo true @@ -61,7 +61,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { ) // When - val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) + val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) // Then result shouldBeEqualTo false @@ -78,7 +78,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { ) // When - val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) + val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) // Then result shouldBeEqualTo false diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCaseTest.kt similarity index 82% rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCaseTest.kt index 508a05acd67..64beb7b8e2e 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCaseTest.kt @@ -23,12 +23,12 @@ import org.junit.Test private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyTogglePushNotificationsOfDevices = true) -class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest { +class CheckIfCanToggleNotificationsViaPusherUseCaseTest { private val fakeSession = FakeSession() - private val checkIfCanTogglePushNotificationsViaPusherUseCase = - CheckIfCanTogglePushNotificationsViaPusherUseCase() + private val checkIfCanToggleNotificationsViaPusherUseCase = + CheckIfCanToggleNotificationsViaPusherUseCase() @Test fun `given current session when execute then toggle capability is returned`() { @@ -38,7 +38,7 @@ class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest { .givenCapabilities(A_HOMESERVER_CAPABILITIES) // When - val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) + val result = checkIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) // Then result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt index b38367b098c..7c0b0a86938 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt @@ -46,15 +46,15 @@ class GetNotificationsStatusUseCaseTest { val instantTaskExecutorRule = InstantTaskExecutorRule() private val fakeSession = FakeSession() - private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase = - mockk() - private val fakeCanTogglePushNotificationsViaPusherUseCase = - mockk() + private val fakeCheckIfCanToggleNotificationsViaAccountDataUseCase = + mockk() + private val fakeCanToggleNotificationsViaPusherUseCase = + mockk() private val getNotificationsStatusUseCase = GetNotificationsStatusUseCase( - checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase, - canTogglePushNotificationsViaPusherUseCase = fakeCanTogglePushNotificationsViaPusherUseCase, + checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase, + canTogglePushNotificationsViaPusherUseCase = fakeCanToggleNotificationsViaPusherUseCase, ) @Before @@ -70,8 +70,8 @@ class GetNotificationsStatusUseCaseTest { @Test fun `given current session and toggle is not supported when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { // Given - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false - every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false) + every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false + every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false) // When val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID) @@ -80,8 +80,8 @@ class GetNotificationsStatusUseCaseTest { result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED verifyOrder { // we should first check account data - fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) - fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) + fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) + fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } } @@ -95,8 +95,8 @@ class GetNotificationsStatusUseCaseTest { ) ) fakeSession.pushersService().givenPushersLive(pushers) - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false - every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true) + every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false + every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true) // When val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID) @@ -116,8 +116,8 @@ class GetNotificationsStatusUseCaseTest { isSilenced = false ).toContent(), ) - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns true - every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false) + every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns true + every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false) // When val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt similarity index 70% rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt index e443dc3e942..99be7c7ea9a 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt @@ -26,21 +26,21 @@ import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent -class TogglePushNotificationUseCaseTest { +class ToggleNotificationUseCaseTest { private val activeSessionHolder = FakeActiveSessionHolder() - private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = - mockk() - private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase = - mockk() + private val fakeCheckIfCanToggleNotificationsViaPusherUseCase = + mockk() + private val fakeCheckIfCanToggleNotificationsViaAccountDataUseCase = + mockk() private val fakeSetNotificationSettingsAccountDataUseCase = mockk() - private val togglePushNotificationUseCase = - TogglePushNotificationUseCase( + private val toggleNotificationUseCase = + ToggleNotificationUseCase( activeSessionHolder = activeSessionHolder.instance, - checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, - checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase, + checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase, + checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase, setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, ) @@ -55,11 +55,11 @@ class TogglePushNotificationUseCaseTest { val fakeSession = activeSessionHolder.fakeSession fakeSession.pushersService().givenPushersLive(pushers) fakeSession.pushersService().givenGetPushers(pushers) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns false + every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns true + every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns false // When - togglePushNotificationUseCase.execute(sessionId, true) + toggleNotificationUseCase.execute(sessionId, true) // Then activeSessionHolder.fakeSession.pushersService().verifyTogglePusherCalled(pushers.first(), true) @@ -70,15 +70,15 @@ class TogglePushNotificationUseCaseTest { // Given val sessionId = "a_session_id" val fakeSession = activeSessionHolder.fakeSession - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true + every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns false + every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any()) } val expectedLocalNotificationSettingsContent = LocalNotificationSettingsContent( isSilenced = false ) // When - togglePushNotificationUseCase.execute(sessionId, true) + toggleNotificationUseCase.execute(sessionId, true) // Then coVerify { diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt index a84aa4b055a..153b79f1a89 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt @@ -16,6 +16,8 @@ package im.vector.app.features.settings.notifications +import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase +import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase @@ -34,15 +36,15 @@ class DisableNotificationsForCurrentSessionUseCaseTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakePushersManager = FakePushersManager() - private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk() - private val fakeTogglePushNotificationUseCase = mockk() + private val fakeCheckIfCanToggleNotificationsViaPusherUseCase = mockk() + private val fakeToggleNotificationUseCase = mockk() private val fakeUnregisterUnifiedPushUseCase = mockk() private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase( activeSessionHolder = fakeActiveSessionHolder.instance, pushersManager = fakePushersManager.instance, - checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, - togglePushNotificationUseCase = fakeTogglePushNotificationUseCase, + checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase, + toggleNotificationUseCase = fakeToggleNotificationUseCase, unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase, ) @@ -51,14 +53,14 @@ class DisableNotificationsForCurrentSessionUseCaseTest { // Given val fakeSession = fakeActiveSessionHolder.fakeSession fakeSession.givenSessionId(A_SESSION_ID) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true - coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) } + every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns true + coJustRun { fakeToggleNotificationUseCase.execute(A_SESSION_ID, any()) } // When disableNotificationsForCurrentSessionUseCase.execute() // Then - coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, false) } + coVerify { fakeToggleNotificationUseCase.execute(A_SESSION_ID, false) } } @Test @@ -66,8 +68,8 @@ class DisableNotificationsForCurrentSessionUseCaseTest { // Given val fakeSession = fakeActiveSessionHolder.fakeSession fakeSession.givenSessionId(A_SESSION_ID) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false - coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) } + every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns false + coJustRun { fakeToggleNotificationUseCase.execute(A_SESSION_ID, any()) } coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) } // When @@ -75,7 +77,7 @@ class DisableNotificationsForCurrentSessionUseCaseTest { // Then coVerify { - fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, false) + fakeToggleNotificationUseCase.execute(A_SESSION_ID, false) fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance) } } diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt index c923f0c7d61..f10b0777cbe 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt @@ -18,7 +18,7 @@ package im.vector.app.features.settings.notifications import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase import im.vector.app.core.pushers.RegisterUnifiedPushUseCase -import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePushersManager import io.mockk.coJustRun @@ -36,14 +36,14 @@ class EnableNotificationsForCurrentSessionUseCaseTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakePushersManager = FakePushersManager() - private val fakeTogglePushNotificationUseCase = mockk() + private val fakeToggleNotificationUseCase = mockk() private val fakeRegisterUnifiedPushUseCase = mockk() private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk() private val enableNotificationsForCurrentSessionUseCase = EnableNotificationsForCurrentSessionUseCase( activeSessionHolder = fakeActiveSessionHolder.instance, pushersManager = fakePushersManager.instance, - togglePushNotificationUseCase = fakeTogglePushNotificationUseCase, + toggleNotificationUseCase = fakeToggleNotificationUseCase, registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase, ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase, ) @@ -57,7 +57,7 @@ class EnableNotificationsForCurrentSessionUseCaseTest { fakePushersManager.givenGetPusherForCurrentSessionReturns(null) every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) } - coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) } + coJustRun { fakeToggleNotificationUseCase.execute(A_SESSION_ID, any()) } // When val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor) diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt index bfbbb877057..cc42193fe1a 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt @@ -16,14 +16,14 @@ package im.vector.app.test.fakes -import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.mockk class FakeTogglePushNotificationUseCase { - val instance = mockk { + val instance = mockk { coJustRun { execute(any(), any()) } } From ab6a6b53c88b4336bb3b96edced8beb852399c1f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 18 Nov 2022 15:19:34 +0100 Subject: [PATCH 087/197] Some refactorings + update unit tests --- .../ConfigureAndStartSessionUseCase.kt | 17 +++- ...oggleNotificationsViaAccountDataUseCase.kt | 14 +-- ...eNotificationSettingsAccountDataUseCase.kt | 11 ++- ...tNotificationSettingsAccountDataUseCase.kt | 34 +++++++ ...tNotificationSettingsAccountDataUseCase.kt | 9 +- .../notification/ToggleNotificationUseCase.kt | 2 +- ...NotificationSettingsAccountDataUseCase.kt} | 21 ++-- .../ConfigureAndStartSessionUseCaseTest.kt | 56 +++++++---- ...eNotificationsViaAccountDataUseCaseTest.kt | 32 +++--- ...ificationSettingsAccountDataUseCaseTest.kt | 59 ----------- ...ificationSettingsAccountDataUseCaseTest.kt | 9 +- ...ificationSettingsAccountDataUseCaseTest.kt | 49 ++++++++++ .../GetNotificationsStatusUseCaseTest.kt | 4 +- ...ificationSettingsAccountDataUseCaseTest.kt | 14 +-- .../ToggleNotificationUseCaseTest.kt | 8 +- ...ificationSettingsAccountDataUseCaseTest.kt | 97 +++++++++++++++++++ .../overview/SessionOverviewViewModelTest.kt | 8 +- ...tificationsForCurrentSessionUseCaseTest.kt | 4 +- ...se.kt => FakeToggleNotificationUseCase.kt} | 2 +- .../app/test/fakes/FakeVectorPreferences.kt | 4 + 20 files changed, 300 insertions(+), 154 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCase.kt rename vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/{CreateNotificationSettingsAccountDataUseCase.kt => UpdateNotificationSettingsAccountDataUseCase.kt} (57%) delete mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt rename vector/src/test/java/im/vector/app/test/fakes/{FakeTogglePushNotificationUseCase.kt => FakeToggleNotificationUseCase.kt} (96%) diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt index 96c3f8a6ce2..d9688a45ed2 100644 --- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt @@ -25,6 +25,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.sync.SyncUtils +import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import timber.log.Timber @@ -36,6 +37,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor( private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase, private val vectorPreferences: VectorPreferences, private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater, + private val updateNotificationSettingsAccountDataUseCase: UpdateNotificationSettingsAccountDataUseCase, ) { fun execute(session: Session, startSyncing: Boolean = true) { @@ -49,11 +51,24 @@ class ConfigureAndStartSessionUseCase @Inject constructor( } session.pushersService().refreshPushers() webRtcCallManager.checkForProtocolsSupportIfNeeded() + updateMatrixClientInfoIfNeeded(session) + createNotificationSettingsAccountDataIfNeeded(session) + enableNotificationsSettingUpdater.onSessionsStarted(session) + } + + private fun updateMatrixClientInfoIfNeeded(session: Session) { session.coroutineScope.launch { if (vectorPreferences.isClientInfoRecordingEnabled()) { updateMatrixClientInfoUseCase.execute(session) } } - enableNotificationsSettingUpdater.onSessionsStarted(session) + } + + private fun createNotificationSettingsAccountDataIfNeeded(session: Session) { + session.coroutineScope.launch { + if (vectorPreferences.isBackgroundSyncEnabled()) { + updateNotificationSettingsAccountDataUseCase.execute(session) + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt index b006e3da452..58289495a40 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt @@ -16,20 +16,14 @@ package im.vector.app.features.settings.devices.v2.notification -import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes -import org.matrix.android.sdk.api.session.events.model.toModel import javax.inject.Inject -class CheckIfCanToggleNotificationsViaAccountDataUseCase @Inject constructor() { +class CheckIfCanToggleNotificationsViaAccountDataUseCase @Inject constructor( + private val getNotificationSettingsAccountDataUseCase: GetNotificationSettingsAccountDataUseCase, +) { fun execute(session: Session, deviceId: String): Boolean { - return session - .accountDataService() - .getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) - ?.content - .toModel() - ?.isSilenced != null + return getNotificationSettingsAccountDataUseCase.execute(session, deviceId)?.isSilenced != null } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt index 51c24e500c1..d71eebdf8ad 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt @@ -17,17 +17,22 @@ package im.vector.app.features.settings.devices.v2.notification import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.Session import javax.inject.Inject +/** + * Delete the content of any associated notification settings to the current session. + */ class DeleteNotificationSettingsAccountDataUseCase @Inject constructor( private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase, ) { - // TODO to be called when switching to push notifications method - suspend fun execute(deviceId: String) { + // TODO to be called when switching to push notifications method (check notification method setting) + suspend fun execute(session: Session) { + val deviceId = session.sessionParams.deviceId ?: return val emptyNotificationSettingsContent = LocalNotificationSettingsContent( isSilenced = null ) - setNotificationSettingsAccountDataUseCase.execute(deviceId, emptyNotificationSettingsContent) + setNotificationSettingsAccountDataUseCase.execute(session, deviceId, emptyNotificationSettingsContent) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCase.kt new file mode 100644 index 00000000000..5517fa09782 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCase.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toModel +import javax.inject.Inject + +class GetNotificationSettingsAccountDataUseCase @Inject constructor() { + + fun execute(session: Session, deviceId: String): LocalNotificationSettingsContent? { + return session + .accountDataService() + .getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) + ?.content + .toModel() + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt index f0ec9d5ddcc..7306794f163 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt @@ -16,18 +16,15 @@ package im.vector.app.features.settings.devices.v2.notification -import im.vector.app.core.di.ActiveSessionHolder import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.toContent import javax.inject.Inject -class SetNotificationSettingsAccountDataUseCase @Inject constructor( - private val activeSessionHolder: ActiveSessionHolder, -) { +class SetNotificationSettingsAccountDataUseCase @Inject constructor() { - suspend fun execute(deviceId: String, localNotificationSettingsContent: LocalNotificationSettingsContent) { - val session = activeSessionHolder.getSafeActiveSession() ?: return + suspend fun execute(session: Session, deviceId: String, localNotificationSettingsContent: LocalNotificationSettingsContent) { session.accountDataService().updateUserAccountData( UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId, localNotificationSettingsContent.toContent(), diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt index d0e1ea2a7a4..73a81a6de17 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt @@ -39,7 +39,7 @@ class ToggleNotificationUseCase @Inject constructor( if (checkIfCanToggleNotificationsViaAccountDataUseCase.execute(session, deviceId)) { val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled) - setNotificationSettingsAccountDataUseCase.execute(deviceId, newNotificationSettingsContent) + setNotificationSettingsAccountDataUseCase.execute(session, deviceId, newNotificationSettingsContent) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt similarity index 57% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt index e2ee19e5cdc..596be90abb6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt @@ -21,18 +21,25 @@ import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent import org.matrix.android.sdk.api.session.Session import javax.inject.Inject -class CreateNotificationSettingsAccountDataUseCase @Inject constructor( +/** + * Update the notification settings account data for the current session. + */ +class UpdateNotificationSettingsAccountDataUseCase @Inject constructor( private val vectorPreferences: VectorPreferences, + private val getNotificationSettingsAccountDataUseCase: GetNotificationSettingsAccountDataUseCase, private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase ) { - // TODO to be called on session start when background sync is enabled + when switching to background sync + // TODO to be called when switching to background sync (in notification method setting) suspend fun execute(session: Session) { val deviceId = session.sessionParams.deviceId ?: return - val isSilenced = !vectorPreferences.areNotificationEnabledForDevice() - val notificationSettingsContent = LocalNotificationSettingsContent( - isSilenced = isSilenced - ) - setNotificationSettingsAccountDataUseCase.execute(deviceId, notificationSettingsContent) + val isSilencedLocal = !vectorPreferences.areNotificationEnabledForDevice() + val isSilencedRemote = getNotificationSettingsAccountDataUseCase.execute(session, deviceId)?.isSilenced + if (isSilencedLocal != isSilencedRemote) { + val notificationSettingsContent = LocalNotificationSettingsContent( + isSilenced = isSilencedLocal + ) + setNotificationSettingsAccountDataUseCase.execute(session, deviceId, notificationSettingsContent) + } } } diff --git a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt index 01596e796de..23a3629efe1 100644 --- a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt @@ -20,6 +20,7 @@ import im.vector.app.core.extensions.startSyncing import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase import im.vector.app.features.session.coroutineScope import im.vector.app.features.sync.SyncUtils +import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeEnableNotificationsSettingUpdater import im.vector.app.test.fakes.FakeSession @@ -47,6 +48,7 @@ class ConfigureAndStartSessionUseCaseTest { private val fakeUpdateMatrixClientInfoUseCase = mockk() private val fakeVectorPreferences = FakeVectorPreferences() private val fakeEnableNotificationsSettingUpdater = FakeEnableNotificationsSettingUpdater() + private val fakeUpdateNotificationSettingsAccountDataUseCase = mockk() private val configureAndStartSessionUseCase = ConfigureAndStartSessionUseCase( context = fakeContext.instance, @@ -54,6 +56,7 @@ class ConfigureAndStartSessionUseCaseTest { updateMatrixClientInfoUseCase = fakeUpdateMatrixClientInfoUseCase, vectorPreferences = fakeVectorPreferences.instance, enableNotificationsSettingUpdater = fakeEnableNotificationsSettingUpdater.instance, + updateNotificationSettingsAccountDataUseCase = fakeUpdateNotificationSettingsAccountDataUseCase, ) @Before @@ -68,47 +71,55 @@ class ConfigureAndStartSessionUseCaseTest { } @Test - fun `given start sync needed and client info recording enabled when execute then it should be configured properly`() = runTest { + fun `given start sync needed and enabled related preferences when execute then it should be configured properly`() = runTest { // Given - val fakeSession = givenASession() - every { fakeSession.coroutineScope } returns this + val aSession = givenASession() + every { aSession.coroutineScope } returns this fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } + coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true) - fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession) + fakeVectorPreferences.givenIsBackgroundSyncEnabled(isEnabled = true) + fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(aSession) // When - configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true) + configureAndStartSessionUseCase.execute(aSession, startSyncing = true) advanceUntilIdle() // Then - verify { fakeSession.startSyncing(fakeContext.instance) } - fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder()) - fakeSession.fakePushersService.verifyRefreshPushers() + verify { aSession.startSyncing(fakeContext.instance) } + aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder()) + aSession.fakePushersService.verifyRefreshPushers() fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded() - coVerify { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) } + coVerify { + fakeUpdateMatrixClientInfoUseCase.execute(aSession) + fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) + } } @Test - fun `given start sync needed and client info recording disabled when execute then it should be configured properly`() = runTest { + fun `given start sync needed and disabled related preferences when execute then it should be configured properly`() = runTest { // Given - val fakeSession = givenASession() - every { fakeSession.coroutineScope } returns this + val aSession = givenASession() + every { aSession.coroutineScope } returns this fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() - coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = false) - fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession) + fakeVectorPreferences.givenIsBackgroundSyncEnabled(isEnabled = false) + fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(aSession) // When - configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true) + configureAndStartSessionUseCase.execute(aSession, startSyncing = true) advanceUntilIdle() // Then - verify { fakeSession.startSyncing(fakeContext.instance) } - fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder()) - fakeSession.fakePushersService.verifyRefreshPushers() + verify { aSession.startSyncing(fakeContext.instance) } + aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder()) + aSession.fakePushersService.verifyRefreshPushers() fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded() - coVerify(inverse = true) { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) } + coVerify(inverse = true) { + fakeUpdateMatrixClientInfoUseCase.execute(aSession) + fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) + } } @Test @@ -118,7 +129,9 @@ class ConfigureAndStartSessionUseCaseTest { every { fakeSession.coroutineScope } returns this fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } + coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true) + fakeVectorPreferences.givenIsBackgroundSyncEnabled(isEnabled = true) fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession) // When @@ -130,7 +143,10 @@ class ConfigureAndStartSessionUseCaseTest { fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder()) fakeSession.fakePushersService.verifyRefreshPushers() fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded() - coVerify { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) } + coVerify { + fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) + fakeUpdateNotificationSettingsAccountDataUseCase.execute(fakeSession) + } } private fun givenASession(): FakeSession { diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt index de225d36ac6..f97e326a022 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt @@ -17,31 +17,29 @@ package im.vector.app.features.settings.devices.v2.notification import im.vector.app.test.fakes.FakeSession +import io.mockk.every import io.mockk.mockk import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes -import org.matrix.android.sdk.api.session.events.model.toContent private const val A_DEVICE_ID = "device-id" class CheckIfCanToggleNotificationsViaAccountDataUseCaseTest { + private val fakeGetNotificationSettingsAccountDataUseCase = mockk() private val fakeSession = FakeSession() private val checkIfCanToggleNotificationsViaAccountDataUseCase = - CheckIfCanToggleNotificationsViaAccountDataUseCase() + CheckIfCanToggleNotificationsViaAccountDataUseCase( + getNotificationSettingsAccountDataUseCase = fakeGetNotificationSettingsAccountDataUseCase, + ) @Test fun `given current session and an account data with a content for the device id when execute then result is true`() { // Given - fakeSession - .accountDataService() - .givenGetUserAccountDataEventReturns( - type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, - content = LocalNotificationSettingsContent(isSilenced = true).toContent(), - ) + val content = LocalNotificationSettingsContent(isSilenced = true) + every { fakeGetNotificationSettingsAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns content // When val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) @@ -53,12 +51,8 @@ class CheckIfCanToggleNotificationsViaAccountDataUseCaseTest { @Test fun `given current session and an account data with empty content for the device id when execute then result is false`() { // Given - fakeSession - .accountDataService() - .givenGetUserAccountDataEventReturns( - type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, - content = mockk(), - ) + val content = LocalNotificationSettingsContent(isSilenced = null) + every { fakeGetNotificationSettingsAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns content // When val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) @@ -70,12 +64,8 @@ class CheckIfCanToggleNotificationsViaAccountDataUseCaseTest { @Test fun `given current session and NO account data for the device id when execute then result is false`() { // Given - fakeSession - .accountDataService() - .givenGetUserAccountDataEventReturns( - type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, - content = null, - ) + val content = null + every { fakeGetNotificationSettingsAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns content // When val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt deleted file mode 100644 index e4cadaa0052..00000000000 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.settings.devices.v2.notification - -import im.vector.app.test.fakes.FakeSession -import im.vector.app.test.fakes.FakeVectorPreferences -import io.mockk.coJustRun -import io.mockk.coVerify -import io.mockk.mockk -import io.mockk.verify -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent - -class CreateNotificationSettingsAccountDataUseCaseTest { - - private val fakeVectorPreferences = FakeVectorPreferences() - private val fakeSetNotificationSettingsAccountDataUseCase = mockk() - - private val createNotificationSettingsAccountDataUseCase = CreateNotificationSettingsAccountDataUseCase( - vectorPreferences = fakeVectorPreferences.instance, - setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, - ) - - @Test - fun `given a device id when execute then content with the current notification preference is set for the account data`() = runTest { - // Given - val aDeviceId = "device-id" - val session = FakeSession() - session.givenSessionId(aDeviceId) - coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any()) } - val areNotificationsEnabled = true - fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled) - val expectedContent = LocalNotificationSettingsContent( - isSilenced = !areNotificationsEnabled - ) - - // When - createNotificationSettingsAccountDataUseCase.execute(session) - - // Then - verify { fakeVectorPreferences.instance.areNotificationEnabledForDevice() } - coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aDeviceId, expectedContent) } - } -} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt index 038a1f436a0..d84ff8c6ac3 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2.notification +import im.vector.app.test.fakes.FakeSession import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.mockk @@ -35,15 +36,17 @@ class DeleteNotificationSettingsAccountDataUseCaseTest { fun `given a device id when execute then empty content is set for the account data`() = runTest { // Given val aDeviceId = "device-id" - coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any()) } + val aSession = FakeSession() + aSession.givenSessionId(aDeviceId) + coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } val expectedContent = LocalNotificationSettingsContent( isSilenced = null ) // When - deleteNotificationSettingsAccountDataUseCase.execute(aDeviceId) + deleteNotificationSettingsAccountDataUseCase.execute(aSession) // Then - coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aDeviceId, expectedContent) } + coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) } } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt new file mode 100644 index 00000000000..75179b56791 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeSession +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toContent + +class GetNotificationSettingsAccountDataUseCaseTest { + + private val getNotificationSettingsAccountDataUseCase = GetNotificationSettingsAccountDataUseCase() + + @Test + fun `given a device id when execute then retrieve the account data event corresponding to this id if any`() { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + val expectedContent = LocalNotificationSettingsContent() + aSession + .accountDataService() + .givenGetUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId, + content = expectedContent.toContent(), + ) + + // When + val result = getNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) + + // Then + result shouldBeEqualTo expectedContent + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt index 7c0b0a86938..d4c3aa57889 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt @@ -53,8 +53,8 @@ class GetNotificationsStatusUseCaseTest { private val getNotificationsStatusUseCase = GetNotificationsStatusUseCase( - checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase, - canTogglePushNotificationsViaPusherUseCase = fakeCanToggleNotificationsViaPusherUseCase, + checkIfCanToggleNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase, + canToggleNotificationsViaPusherUseCase = fakeCanToggleNotificationsViaPusherUseCase, ) @Before diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt index 8f72f0946f5..d26271e59db 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt @@ -16,7 +16,7 @@ package im.vector.app.features.settings.devices.v2.notification -import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeSession import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent @@ -25,25 +25,21 @@ import org.matrix.android.sdk.api.session.events.model.toContent class SetNotificationSettingsAccountDataUseCaseTest { - private val activeSessionHolder = FakeActiveSessionHolder() - - private val setNotificationSettingsAccountDataUseCase = SetNotificationSettingsAccountDataUseCase( - activeSessionHolder = activeSessionHolder.instance, - ) + private val setNotificationSettingsAccountDataUseCase = SetNotificationSettingsAccountDataUseCase() @Test fun `given a content when execute then update local notification settings with this content`() = runTest { // Given val sessionId = "a_session_id" val localNotificationSettingsContent = LocalNotificationSettingsContent() - val fakeSession = activeSessionHolder.fakeSession + val fakeSession = FakeSession() fakeSession.accountDataService().givenUpdateUserAccountDataEventSucceeds() // When - setNotificationSettingsAccountDataUseCase.execute(sessionId, localNotificationSettingsContent) + setNotificationSettingsAccountDataUseCase.execute(fakeSession, sessionId, localNotificationSettingsContent) // Then - activeSessionHolder.fakeSession.accountDataService().verifyUpdateUserAccountDataEventSucceeds( + fakeSession.accountDataService().verifyUpdateUserAccountDataEventSucceeds( UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, localNotificationSettingsContent.toContent(), ) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt index 99be7c7ea9a..1e3517c776e 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt @@ -39,8 +39,8 @@ class ToggleNotificationUseCaseTest { private val toggleNotificationUseCase = ToggleNotificationUseCase( activeSessionHolder = activeSessionHolder.instance, - checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase, - checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase, + checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase, + checkIfCanToggleNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase, setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, ) @@ -72,7 +72,7 @@ class ToggleNotificationUseCaseTest { val fakeSession = activeSessionHolder.fakeSession every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns false every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true - coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any()) } + coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } val expectedLocalNotificationSettingsContent = LocalNotificationSettingsContent( isSilenced = false ) @@ -82,7 +82,7 @@ class ToggleNotificationUseCaseTest { // Then coVerify { - fakeSetNotificationSettingsAccountDataUseCase.execute(sessionId, expectedLocalNotificationSettingsContent) + fakeSetNotificationSettingsAccountDataUseCase.execute(fakeSession, sessionId, expectedLocalNotificationSettingsContent) } } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt new file mode 100644 index 00000000000..41c5ab9081a --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.FakeVectorPreferences +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent + +class UpdateNotificationSettingsAccountDataUseCaseTest { + + private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeGetNotificationSettingsAccountDataUseCase = mockk() + private val fakeSetNotificationSettingsAccountDataUseCase = mockk() + + private val updateNotificationSettingsAccountDataUseCase = UpdateNotificationSettingsAccountDataUseCase( + vectorPreferences = fakeVectorPreferences.instance, + getNotificationSettingsAccountDataUseCase = fakeGetNotificationSettingsAccountDataUseCase, + setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, + ) + + @Test + fun `given a device id and a different local setting compared to remote when execute then content is updated`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + aSession.givenSessionId(aDeviceId) + coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } + val areNotificationsEnabled = true + fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled) + every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns + LocalNotificationSettingsContent( + isSilenced = null + ) + val expectedContent = LocalNotificationSettingsContent( + isSilenced = !areNotificationsEnabled + ) + + // When + updateNotificationSettingsAccountDataUseCase.execute(aSession) + + // Then + verify { + fakeVectorPreferences.instance.areNotificationEnabledForDevice() + fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) + } + coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) } + } + + @Test + fun `given a device id and a same local setting compared to remote when execute then content is not updated`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + aSession.givenSessionId(aDeviceId) + coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } + val areNotificationsEnabled = true + fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled) + every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns + LocalNotificationSettingsContent( + isSilenced = false + ) + val expectedContent = LocalNotificationSettingsContent( + isSilenced = !areNotificationsEnabled + ) + + // When + updateNotificationSettingsAccountDataUseCase.execute(aSession) + + // Then + verify { + fakeVectorPreferences.instance.areNotificationEnabledForDevice() + fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) + } + coVerify(inverse = true) { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) } + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 287bdd159c5..901c0331c5b 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -30,7 +30,7 @@ import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase -import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase +import im.vector.app.test.fakes.FakeToggleNotificationUseCase import im.vector.app.test.fakes.FakeVectorPreferences import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test @@ -76,7 +76,7 @@ class SessionOverviewViewModelTest { private val interceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private val refreshDevicesUseCase = mockk(relaxed = true) - private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase() + private val toggleNotificationUseCase = FakeToggleNotificationUseCase() private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase() private val notificationsStatus = NotificationsStatus.ENABLED private val fakeVectorPreferences = FakeVectorPreferences() @@ -91,7 +91,7 @@ class SessionOverviewViewModelTest { pendingAuthHandler = fakePendingAuthHandler.instance, activeSessionHolder = fakeActiveSessionHolder.instance, refreshDevicesUseCase = refreshDevicesUseCase, - togglePushNotificationUseCase = togglePushNotificationUseCase.instance, + toggleNotificationUseCase = toggleNotificationUseCase.instance, getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance, vectorPreferences = fakeVectorPreferences.instance, toggleIpAddressVisibilityUseCase = toggleIpAddressVisibilityUseCase, @@ -436,7 +436,7 @@ class SessionOverviewViewModelTest { viewModel.handle(SessionOverviewAction.TogglePushNotifications(A_SESSION_ID_1, true)) - togglePushNotificationUseCase.verifyExecute(A_SESSION_ID_1, true) + toggleNotificationUseCase.verifyExecute(A_SESSION_ID_1, true) viewModel.test().assertLatestState { state -> state.notificationsStatus == NotificationsStatus.ENABLED }.finish() } } diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt index 153b79f1a89..e53874858a4 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt @@ -16,11 +16,9 @@ package im.vector.app.features.settings.notifications +import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase -import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase -import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase -import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePushersManager import io.mockk.coJustRun diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt similarity index 96% rename from vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt rename to vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt index cc42193fe1a..527625144e0 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt @@ -21,7 +21,7 @@ import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.mockk -class FakeTogglePushNotificationUseCase { +class FakeToggleNotificationUseCase { val instance = mockk { coJustRun { execute(any(), any()) } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index 06efca1bf77..58bc1a18b87 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -73,4 +73,8 @@ class FakeVectorPreferences { fun givenAreNotificationsEnabledForDevice(notificationsEnabled: Boolean) { every { instance.areNotificationEnabledForDevice() } returns notificationsEnabled } + + fun givenIsBackgroundSyncEnabled(isEnabled: Boolean) { + every { instance.isBackgroundSyncEnabled() } returns isEnabled + } } From e99dc1d163008123520b5e9cfb58d7ac7795fef9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 18 Nov 2022 17:17:05 +0100 Subject: [PATCH 088/197] Remove unused parameters from some ViewModel --- .../vector/app/features/settings/devices/v2/DevicesViewModel.kt | 1 - .../settings/devices/v2/overview/SessionOverviewViewModel.kt | 1 - .../app/features/settings/devices/v2/DevicesViewModelTest.kt | 2 -- .../devices/v2/overview/SessionOverviewViewModelTest.kt | 2 -- 4 files changed, 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index f42d5af3986..bfccd2f9d38 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -48,7 +48,6 @@ class DevicesViewModel @AssistedInject constructor( private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase, - private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase, private val vectorPreferences: VectorPreferences, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 0ddf688514e..74f962b4645 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -51,7 +51,6 @@ class SessionOverviewViewModel @AssistedInject constructor( private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase, - private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, private val activeSessionHolder: ActiveSessionHolder, private val toggleNotificationUseCase: ToggleNotificationUseCase, diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 03177aac478..aa5ebd73eb7 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -72,7 +72,6 @@ class DevicesViewModelTest { private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk(relaxed = true) private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() - private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private val fakeRefreshDevicesUseCase = mockk(relaxUnitFun = true) private val fakeVectorPreferences = FakeVectorPreferences() @@ -87,7 +86,6 @@ class DevicesViewModelTest { refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, - interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCase, vectorPreferences = fakeVectorPreferences.instance, diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 901c0331c5b..2ddc91cdace 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -73,7 +73,6 @@ class SessionOverviewViewModelTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() - private val interceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private val refreshDevicesUseCase = mockk(relaxed = true) private val toggleNotificationUseCase = FakeToggleNotificationUseCase() @@ -87,7 +86,6 @@ class SessionOverviewViewModelTest { getDeviceFullInfoUseCase = getDeviceFullInfoUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, - interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, activeSessionHolder = fakeActiveSessionHolder.instance, refreshDevicesUseCase = refreshDevicesUseCase, From 637961bbb1043fd39ac0844d09eeb65dc7b30f0a Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 23 Nov 2022 14:45:37 +0100 Subject: [PATCH 089/197] Update related account data event on notification method change --- .../LocalNotificationSettingsContent.kt | 2 +- .../EnableNotificationsSettingUpdater.kt | 39 ---------- .../NotificationsSettingUpdater.kt | 74 +++++++++++++++++++ .../ConfigureAndStartSessionUseCase.kt | 10 +-- ...eNotificationSettingsAccountDataUseCase.kt | 12 +-- ...eNotificationSettingsAccountDataUseCase.kt | 27 +++++-- .../ConfigureAndStartSessionUseCaseTest.kt | 38 +++++----- ...ificationSettingsAccountDataUseCaseTest.kt | 28 ++++++- ...ificationSettingsAccountDataUseCaseTest.kt | 22 +++++- ...ificationSettingsAccountDataUseCaseTest.kt | 2 +- ...ificationSettingsAccountDataUseCaseTest.kt | 32 +++++++- ....kt => FakeNotificationsSettingUpdater.kt} | 6 +- 12 files changed, 207 insertions(+), 85 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt create mode 100644 vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt rename vector/src/test/java/im/vector/app/test/fakes/{FakeEnableNotificationsSettingUpdater.kt => FakeNotificationsSettingUpdater.kt} (82%) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt index 6998d9dcf27..75d04f340a5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt @@ -22,5 +22,5 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class LocalNotificationSettingsContent( @Json(name = "is_silenced") - val isSilenced: Boolean? = false + val isSilenced: Boolean? ) diff --git a/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt deleted file mode 100644 index 81b524cde96..00000000000 --- a/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.core.notification - -import im.vector.app.features.session.coroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.session.Session -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class EnableNotificationsSettingUpdater @Inject constructor( - private val updateEnableNotificationsSettingOnChangeUseCase: UpdateEnableNotificationsSettingOnChangeUseCase, -) { - - private var job: Job? = null - - fun onSessionsStarted(session: Session) { - job?.cancel() - job = session.coroutineScope.launch { - updateEnableNotificationsSettingOnChangeUseCase.execute(session) - } - } -} diff --git a/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt new file mode 100644 index 00000000000..c6738edddb2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.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.core.notification + +import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import im.vector.app.features.session.coroutineScope +import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.settings.VectorPreferences.Companion.SETTINGS_FDROID_BACKGROUND_SYNC_MODE +import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Listen changes in Pusher or Account Data to update the local setting for notification toggle. + * Listen changes on background sync mode preference to update the corresponding Account Data event. + */ +@Singleton +class NotificationsSettingUpdater @Inject constructor( + private val updateEnableNotificationsSettingOnChangeUseCase: UpdateEnableNotificationsSettingOnChangeUseCase, + private val vectorPreferences: VectorPreferences, + private val updateNotificationSettingsAccountDataUseCase: UpdateNotificationSettingsAccountDataUseCase, +) { + + private var job: Job? = null + private var prefChangeListener: OnSharedPreferenceChangeListener? = null + + // TODO add unit tests + fun onSessionsStarted(session: Session) { + updateEnableNotificationsSettingOnChange(session) + updateNotificationSettingsAccountDataOnChange(session) + } + + private fun updateEnableNotificationsSettingOnChange(session: Session) { + job?.cancel() + job = session.coroutineScope.launch { + updateEnableNotificationsSettingOnChangeUseCase.execute(session) + } + } + + private fun updateNotificationSettingsAccountDataOnChange(session: Session) { + prefChangeListener?.let { vectorPreferences.unsubscribeToChanges(it) } + prefChangeListener = null + prefChangeListener = createPrefListener(session).also { + vectorPreferences.subscribeToChanges(it) + } + } + + private fun createPrefListener(session: Session): OnSharedPreferenceChangeListener { + return OnSharedPreferenceChangeListener { _, key -> + session.coroutineScope.launch { + if (key == SETTINGS_FDROID_BACKGROUND_SYNC_MODE) { + updateNotificationSettingsAccountDataUseCase.execute(session) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt index d9688a45ed2..623f7d83a9e 100644 --- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt @@ -19,7 +19,7 @@ package im.vector.app.core.session import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import im.vector.app.core.extensions.startSyncing -import im.vector.app.core.notification.EnableNotificationsSettingUpdater +import im.vector.app.core.notification.NotificationsSettingUpdater import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.session.coroutineScope @@ -36,7 +36,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor( private val webRtcCallManager: WebRtcCallManager, private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase, private val vectorPreferences: VectorPreferences, - private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater, + private val notificationsSettingUpdater: NotificationsSettingUpdater, private val updateNotificationSettingsAccountDataUseCase: UpdateNotificationSettingsAccountDataUseCase, ) { @@ -53,7 +53,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor( webRtcCallManager.checkForProtocolsSupportIfNeeded() updateMatrixClientInfoIfNeeded(session) createNotificationSettingsAccountDataIfNeeded(session) - enableNotificationsSettingUpdater.onSessionsStarted(session) + notificationsSettingUpdater.onSessionsStarted(session) } private fun updateMatrixClientInfoIfNeeded(session: Session) { @@ -66,9 +66,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor( private fun createNotificationSettingsAccountDataIfNeeded(session: Session) { session.coroutineScope.launch { - if (vectorPreferences.isBackgroundSyncEnabled()) { - updateNotificationSettingsAccountDataUseCase.execute(session) - } + updateNotificationSettingsAccountDataUseCase.execute(session) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt index d71eebdf8ad..3c086fe1113 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt @@ -24,15 +24,17 @@ import javax.inject.Inject * Delete the content of any associated notification settings to the current session. */ class DeleteNotificationSettingsAccountDataUseCase @Inject constructor( + private val getNotificationSettingsAccountDataUseCase: GetNotificationSettingsAccountDataUseCase, private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase, ) { - // TODO to be called when switching to push notifications method (check notification method setting) suspend fun execute(session: Session) { val deviceId = session.sessionParams.deviceId ?: return - val emptyNotificationSettingsContent = LocalNotificationSettingsContent( - isSilenced = null - ) - setNotificationSettingsAccountDataUseCase.execute(session, deviceId, emptyNotificationSettingsContent) + if (getNotificationSettingsAccountDataUseCase.execute(session, deviceId)?.isSilenced != null) { + val emptyNotificationSettingsContent = LocalNotificationSettingsContent( + isSilenced = null + ) + setNotificationSettingsAccountDataUseCase.execute(session, deviceId, emptyNotificationSettingsContent) + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt index 596be90abb6..7791c1dd4b9 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt @@ -22,24 +22,37 @@ import org.matrix.android.sdk.api.session.Session import javax.inject.Inject /** - * Update the notification settings account data for the current session. + * Update the notification settings account data for the current session depending on whether + * the background sync is enabled or not. */ class UpdateNotificationSettingsAccountDataUseCase @Inject constructor( private val vectorPreferences: VectorPreferences, private val getNotificationSettingsAccountDataUseCase: GetNotificationSettingsAccountDataUseCase, - private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase + private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase, + private val deleteNotificationSettingsAccountDataUseCase: DeleteNotificationSettingsAccountDataUseCase, ) { - // TODO to be called when switching to background sync (in notification method setting) suspend fun execute(session: Session) { + if (vectorPreferences.isBackgroundSyncEnabled()) { + setCurrentNotificationStatus(session) + } else { + deleteCurrentNotificationStatus(session) + } + } + + private suspend fun setCurrentNotificationStatus(session: Session) { val deviceId = session.sessionParams.deviceId ?: return - val isSilencedLocal = !vectorPreferences.areNotificationEnabledForDevice() - val isSilencedRemote = getNotificationSettingsAccountDataUseCase.execute(session, deviceId)?.isSilenced - if (isSilencedLocal != isSilencedRemote) { + val areNotificationsSilenced = !vectorPreferences.areNotificationEnabledForDevice() + val isSilencedAccountData = getNotificationSettingsAccountDataUseCase.execute(session, deviceId)?.isSilenced + if (areNotificationsSilenced != isSilencedAccountData) { val notificationSettingsContent = LocalNotificationSettingsContent( - isSilenced = isSilencedLocal + isSilenced = areNotificationsSilenced ) setNotificationSettingsAccountDataUseCase.execute(session, deviceId, notificationSettingsContent) } } + + private suspend fun deleteCurrentNotificationStatus(session: Session) { + deleteNotificationSettingsAccountDataUseCase.execute(session) + } } diff --git a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt index 23a3629efe1..4071afaf3f9 100644 --- a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt @@ -22,7 +22,7 @@ import im.vector.app.features.session.coroutineScope import im.vector.app.features.sync.SyncUtils import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase import im.vector.app.test.fakes.FakeContext -import im.vector.app.test.fakes.FakeEnableNotificationsSettingUpdater +import im.vector.app.test.fakes.FakeNotificationsSettingUpdater import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeVectorPreferences import im.vector.app.test.fakes.FakeWebRtcCallManager @@ -47,7 +47,7 @@ class ConfigureAndStartSessionUseCaseTest { private val fakeWebRtcCallManager = FakeWebRtcCallManager() private val fakeUpdateMatrixClientInfoUseCase = mockk() private val fakeVectorPreferences = FakeVectorPreferences() - private val fakeEnableNotificationsSettingUpdater = FakeEnableNotificationsSettingUpdater() + private val fakeNotificationsSettingUpdater = FakeNotificationsSettingUpdater() private val fakeUpdateNotificationSettingsAccountDataUseCase = mockk() private val configureAndStartSessionUseCase = ConfigureAndStartSessionUseCase( @@ -55,7 +55,7 @@ class ConfigureAndStartSessionUseCaseTest { webRtcCallManager = fakeWebRtcCallManager.instance, updateMatrixClientInfoUseCase = fakeUpdateMatrixClientInfoUseCase, vectorPreferences = fakeVectorPreferences.instance, - enableNotificationsSettingUpdater = fakeEnableNotificationsSettingUpdater.instance, + notificationsSettingUpdater = fakeNotificationsSettingUpdater.instance, updateNotificationSettingsAccountDataUseCase = fakeUpdateNotificationSettingsAccountDataUseCase, ) @@ -71,7 +71,7 @@ class ConfigureAndStartSessionUseCaseTest { } @Test - fun `given start sync needed and enabled related preferences when execute then it should be configured properly`() = runTest { + fun `given start sync needed and client info recording enabled when execute then it should be configured properly`() = runTest { // Given val aSession = givenASession() every { aSession.coroutineScope } returns this @@ -79,8 +79,7 @@ class ConfigureAndStartSessionUseCaseTest { coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true) - fakeVectorPreferences.givenIsBackgroundSyncEnabled(isEnabled = true) - fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(aSession) + fakeNotificationsSettingUpdater.givenOnSessionsStarted(aSession) // When configureAndStartSessionUseCase.execute(aSession, startSyncing = true) @@ -98,14 +97,14 @@ class ConfigureAndStartSessionUseCaseTest { } @Test - fun `given start sync needed and disabled related preferences when execute then it should be configured properly`() = runTest { + fun `given start sync needed and client info recording disabled when execute then it should be configured properly`() = runTest { // Given val aSession = givenASession() every { aSession.coroutineScope } returns this fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() + coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = false) - fakeVectorPreferences.givenIsBackgroundSyncEnabled(isEnabled = false) - fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(aSession) + fakeNotificationsSettingUpdater.givenOnSessionsStarted(aSession) // When configureAndStartSessionUseCase.execute(aSession, startSyncing = true) @@ -118,6 +117,8 @@ class ConfigureAndStartSessionUseCaseTest { fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded() coVerify(inverse = true) { fakeUpdateMatrixClientInfoUseCase.execute(aSession) + } + coVerify { fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) } } @@ -125,27 +126,26 @@ class ConfigureAndStartSessionUseCaseTest { @Test fun `given a session and no start sync needed when execute then it should be configured properly`() = runTest { // Given - val fakeSession = givenASession() - every { fakeSession.coroutineScope } returns this + val aSession = givenASession() + every { aSession.coroutineScope } returns this fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true) - fakeVectorPreferences.givenIsBackgroundSyncEnabled(isEnabled = true) - fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession) + fakeNotificationsSettingUpdater.givenOnSessionsStarted(aSession) // When - configureAndStartSessionUseCase.execute(fakeSession, startSyncing = false) + configureAndStartSessionUseCase.execute(aSession, startSyncing = false) advanceUntilIdle() // Then - verify(inverse = true) { fakeSession.startSyncing(fakeContext.instance) } - fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder()) - fakeSession.fakePushersService.verifyRefreshPushers() + verify(inverse = true) { aSession.startSyncing(fakeContext.instance) } + aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder()) + aSession.fakePushersService.verifyRefreshPushers() fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded() coVerify { - fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) - fakeUpdateNotificationSettingsAccountDataUseCase.execute(fakeSession) + fakeUpdateMatrixClientInfoUseCase.execute(aSession) + fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt index d84ff8c6ac3..600ba2ba484 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt @@ -19,7 +19,9 @@ package im.vector.app.features.settings.devices.v2.notification import im.vector.app.test.fakes.FakeSession import io.mockk.coJustRun import io.mockk.coVerify +import io.mockk.every import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent @@ -27,17 +29,22 @@ import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent class DeleteNotificationSettingsAccountDataUseCaseTest { private val fakeSetNotificationSettingsAccountDataUseCase = mockk() + private val fakeGetNotificationSettingsAccountDataUseCase = mockk() private val deleteNotificationSettingsAccountDataUseCase = DeleteNotificationSettingsAccountDataUseCase( setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, + getNotificationSettingsAccountDataUseCase = fakeGetNotificationSettingsAccountDataUseCase, ) @Test - fun `given a device id when execute then empty content is set for the account data`() = runTest { + fun `given a device id and existing account data content when execute then empty content is set for the account data`() = runTest { // Given val aDeviceId = "device-id" val aSession = FakeSession() aSession.givenSessionId(aDeviceId) + every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns LocalNotificationSettingsContent( + isSilenced = true, + ) coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } val expectedContent = LocalNotificationSettingsContent( isSilenced = null @@ -47,6 +54,25 @@ class DeleteNotificationSettingsAccountDataUseCaseTest { deleteNotificationSettingsAccountDataUseCase.execute(aSession) // Then + verify { fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) } coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) } } + + @Test + fun `given a device id and empty existing account data content when execute then nothing is done`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + aSession.givenSessionId(aDeviceId) + every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns LocalNotificationSettingsContent( + isSilenced = null, + ) + + // When + deleteNotificationSettingsAccountDataUseCase.execute(aSession) + + // Then + verify { fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) } + coVerify(inverse = true) { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, any()) } + } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt index 75179b56791..2adb0d85997 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt @@ -32,7 +32,27 @@ class GetNotificationSettingsAccountDataUseCaseTest { // Given val aDeviceId = "device-id" val aSession = FakeSession() - val expectedContent = LocalNotificationSettingsContent() + val expectedContent = LocalNotificationSettingsContent(isSilenced = true) + aSession + .accountDataService() + .givenGetUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId, + content = expectedContent.toContent(), + ) + + // When + val result = getNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) + + // Then + result shouldBeEqualTo expectedContent + } + + @Test + fun `given a device id and empty content when execute then retrieve the account data event corresponding to this id if any`() { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + val expectedContent = LocalNotificationSettingsContent(isSilenced = null) aSession .accountDataService() .givenGetUserAccountDataEventReturns( diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt index d26271e59db..89fcd5e512b 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt @@ -31,7 +31,7 @@ class SetNotificationSettingsAccountDataUseCaseTest { fun `given a content when execute then update local notification settings with this content`() = runTest { // Given val sessionId = "a_session_id" - val localNotificationSettingsContent = LocalNotificationSettingsContent() + val localNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = true) val fakeSession = FakeSession() fakeSession.accountDataService().givenUpdateUserAccountDataEventSucceeds() diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt index 41c5ab9081a..3bca0da84e3 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt @@ -32,15 +32,17 @@ class UpdateNotificationSettingsAccountDataUseCaseTest { private val fakeVectorPreferences = FakeVectorPreferences() private val fakeGetNotificationSettingsAccountDataUseCase = mockk() private val fakeSetNotificationSettingsAccountDataUseCase = mockk() + private val fakeDeleteNotificationSettingsAccountDataUseCase = mockk() private val updateNotificationSettingsAccountDataUseCase = UpdateNotificationSettingsAccountDataUseCase( vectorPreferences = fakeVectorPreferences.instance, getNotificationSettingsAccountDataUseCase = fakeGetNotificationSettingsAccountDataUseCase, setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, + deleteNotificationSettingsAccountDataUseCase = fakeDeleteNotificationSettingsAccountDataUseCase, ) @Test - fun `given a device id and a different local setting compared to remote when execute then content is updated`() = runTest { + fun `given back sync enabled, a device id and a different local setting compared to remote when execute then content is updated`() = runTest { // Given val aDeviceId = "device-id" val aSession = FakeSession() @@ -48,6 +50,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest { coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } val areNotificationsEnabled = true fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled) + fakeVectorPreferences.givenIsBackgroundSyncEnabled(true) every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns LocalNotificationSettingsContent( isSilenced = null @@ -61,14 +64,16 @@ class UpdateNotificationSettingsAccountDataUseCaseTest { // Then verify { + fakeVectorPreferences.instance.isBackgroundSyncEnabled() fakeVectorPreferences.instance.areNotificationEnabledForDevice() fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) } + coVerify(inverse = true) { fakeDeleteNotificationSettingsAccountDataUseCase.execute(aSession) } coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) } } @Test - fun `given a device id and a same local setting compared to remote when execute then content is not updated`() = runTest { + fun `given back sync enabled, a device id and a same local setting compared to remote when execute then content is not updated`() = runTest { // Given val aDeviceId = "device-id" val aSession = FakeSession() @@ -76,6 +81,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest { coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } val areNotificationsEnabled = true fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled) + fakeVectorPreferences.givenIsBackgroundSyncEnabled(true) every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns LocalNotificationSettingsContent( isSilenced = false @@ -89,9 +95,31 @@ class UpdateNotificationSettingsAccountDataUseCaseTest { // Then verify { + fakeVectorPreferences.instance.isBackgroundSyncEnabled() fakeVectorPreferences.instance.areNotificationEnabledForDevice() fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) } + coVerify(inverse = true) { fakeDeleteNotificationSettingsAccountDataUseCase.execute(aSession) } coVerify(inverse = true) { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) } } + + @Test + fun `given back sync disabled and a device id when execute then content is deleted`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + aSession.givenSessionId(aDeviceId) + coJustRun { fakeDeleteNotificationSettingsAccountDataUseCase.execute(any()) } + fakeVectorPreferences.givenIsBackgroundSyncEnabled(false) + + // When + updateNotificationSettingsAccountDataUseCase.execute(aSession) + + // Then + verify { + fakeVectorPreferences.instance.isBackgroundSyncEnabled() + } + coVerify { fakeDeleteNotificationSettingsAccountDataUseCase.execute(aSession) } + coVerify(inverse = true) { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, any()) } + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt similarity index 82% rename from vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt rename to vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt index a78dd1a34bb..f9f38e6c2a2 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt @@ -16,14 +16,14 @@ package im.vector.app.test.fakes -import im.vector.app.core.notification.EnableNotificationsSettingUpdater +import im.vector.app.core.notification.NotificationsSettingUpdater import io.mockk.justRun import io.mockk.mockk import org.matrix.android.sdk.api.session.Session -class FakeEnableNotificationsSettingUpdater { +class FakeNotificationsSettingUpdater { - val instance = mockk() + val instance = mockk() fun givenOnSessionsStarted(session: Session) { justRun { instance.onSessionsStarted(session) } From 7c10a4cb21a0bc0a044698ea633c5a76626bdbd6 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 23 Nov 2022 15:12:51 +0100 Subject: [PATCH 090/197] Adding tests for notifications setting updater --- .../NotificationsSettingUpdater.kt | 7 +- .../ConfigureAndStartSessionUseCase.kt | 2 +- .../NotificationsSettingUpdaterTest.kt | 106 ++++++++++++++++++ .../fakes/FakeNotificationsSettingUpdater.kt | 2 +- .../app/test/fakes/FakeVectorPreferences.kt | 7 ++ 5 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt diff --git a/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt index c6738edddb2..4a16f37cfeb 100644 --- a/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt +++ b/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt @@ -41,10 +41,9 @@ class NotificationsSettingUpdater @Inject constructor( private var job: Job? = null private var prefChangeListener: OnSharedPreferenceChangeListener? = null - // TODO add unit tests - fun onSessionsStarted(session: Session) { + fun onSessionStarted(session: Session) { updateEnableNotificationsSettingOnChange(session) - updateNotificationSettingsAccountDataOnChange(session) + updateAccountDataOnBackgroundSyncChange(session) } private fun updateEnableNotificationsSettingOnChange(session: Session) { @@ -54,7 +53,7 @@ class NotificationsSettingUpdater @Inject constructor( } } - private fun updateNotificationSettingsAccountDataOnChange(session: Session) { + private fun updateAccountDataOnBackgroundSyncChange(session: Session) { prefChangeListener?.let { vectorPreferences.unsubscribeToChanges(it) } prefChangeListener = null prefChangeListener = createPrefListener(session).also { diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt index 623f7d83a9e..d167b02d059 100644 --- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt @@ -53,7 +53,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor( webRtcCallManager.checkForProtocolsSupportIfNeeded() updateMatrixClientInfoIfNeeded(session) createNotificationSettingsAccountDataIfNeeded(session) - notificationsSettingUpdater.onSessionsStarted(session) + notificationsSettingUpdater.onSessionStarted(session) } private fun updateMatrixClientInfoIfNeeded(session: Session) { diff --git a/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt b/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt new file mode 100644 index 00000000000..386b52e61e6 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt @@ -0,0 +1,106 @@ +/* + * 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.notification + +import im.vector.app.features.session.coroutineScope +import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.FakeVectorPreferences +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class NotificationsSettingUpdaterTest { + + private val fakeUpdateEnableNotificationsSettingOnChangeUseCase = mockk() + private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeUpdateNotificationSettingsAccountDataUseCase = mockk() + + private val notificationsSettingUpdater = NotificationsSettingUpdater( + updateEnableNotificationsSettingOnChangeUseCase = fakeUpdateEnableNotificationsSettingOnChangeUseCase, + vectorPreferences = fakeVectorPreferences.instance, + updateNotificationSettingsAccountDataUseCase = fakeUpdateNotificationSettingsAccountDataUseCase, + ) + + @Before + fun setup() { + mockkStatic("im.vector.app.features.session.SessionCoroutineScopesKt") + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a session when calling onSessionStarted then update enable notification on change`() = runTest { + // Given + val aSession = FakeSession() + every { aSession.coroutineScope } returns this + coJustRun { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(any()) } + + // When + notificationsSettingUpdater.onSessionStarted(aSession) + advanceUntilIdle() + + // Then + coVerify { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(aSession) } + } + + @Test + fun `given a session when calling onSessionStarted then update account data on background sync preference change`() = runTest { + // Given + val aSession = FakeSession() + every { aSession.coroutineScope } returns this + coJustRun { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(any()) } + coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) } + fakeVectorPreferences.givenChangeOnPreference(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE) + + // When + notificationsSettingUpdater.onSessionStarted(aSession) + advanceUntilIdle() + + // Then + coVerify { fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) } + } + + @Test + fun `given a session when calling onSessionStarted then account data is not updated on other preference change`() = runTest { + // Given + val aSession = FakeSession() + every { aSession.coroutineScope } returns this + coJustRun { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(any()) } + coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) } + fakeVectorPreferences.givenChangeOnPreference("key") + + // When + notificationsSettingUpdater.onSessionStarted(aSession) + advanceUntilIdle() + + // Then + coVerify(inverse = true) { fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt index f9f38e6c2a2..2e397763f81 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt @@ -26,6 +26,6 @@ class FakeNotificationsSettingUpdater { val instance = mockk() fun givenOnSessionsStarted(session: Session) { - justRun { instance.onSessionsStarted(session) } + justRun { instance.onSessionStarted(session) } } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index 58bc1a18b87..94bc0966c56 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -17,6 +17,7 @@ package im.vector.app.test.fakes import im.vector.app.features.settings.BackgroundSyncMode +import android.content.SharedPreferences.OnSharedPreferenceChangeListener import im.vector.app.features.settings.VectorPreferences import io.mockk.every import io.mockk.justRun @@ -77,4 +78,10 @@ class FakeVectorPreferences { fun givenIsBackgroundSyncEnabled(isEnabled: Boolean) { every { instance.isBackgroundSyncEnabled() } returns isEnabled } + + fun givenChangeOnPreference(key: String) { + every { instance.subscribeToChanges(any()) } answers { + firstArg().onSharedPreferenceChanged(mockk(), key) + } + } } From a2ae3af69d5c187930e0e0f6e4884eb3b4f29de2 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 23 Nov 2022 15:23:31 +0100 Subject: [PATCH 091/197] Removing unused imports --- .../vector/app/features/settings/devices/v2/DevicesViewModel.kt | 1 - .../settings/devices/v2/overview/SessionOverviewViewModel.kt | 1 - .../app/features/settings/devices/v2/DevicesViewModelTest.kt | 1 - .../settings/devices/v2/overview/SessionOverviewViewModelTest.kt | 1 - 4 files changed, 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index bfccd2f9d38..b7a6c5df30b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -28,7 +28,6 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 74f962b4645..f598c397de6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -32,7 +32,6 @@ import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCa import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index aa5ebd73eb7..4bfd5c44966 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -22,7 +22,6 @@ import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.core.session.clientinfo.MatrixClientInfoContent import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo import im.vector.app.features.settings.devices.v2.list.DeviceType -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 2ddc91cdace..6018152176f 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -24,7 +24,6 @@ import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCase import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase From 68d00e00d16b7082f6f86e2c73120387b0eb762e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 24 Nov 2022 18:01:34 +0100 Subject: [PATCH 092/197] Fix method used to check if background sync is enabled --- ...pdateNotificationSettingsAccountDataUseCase.kt | 4 +++- ...eNotificationSettingsAccountDataUseCaseTest.kt | 15 +++++++++------ .../app/test/fakes/FakeUnifiedPushHelper.kt | 4 ++++ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt index 7791c1dd4b9..9296bcd9129 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2.notification +import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent import org.matrix.android.sdk.api.session.Session @@ -27,13 +28,14 @@ import javax.inject.Inject */ class UpdateNotificationSettingsAccountDataUseCase @Inject constructor( private val vectorPreferences: VectorPreferences, + private val unifiedPushHelper: UnifiedPushHelper, private val getNotificationSettingsAccountDataUseCase: GetNotificationSettingsAccountDataUseCase, private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase, private val deleteNotificationSettingsAccountDataUseCase: DeleteNotificationSettingsAccountDataUseCase, ) { suspend fun execute(session: Session) { - if (vectorPreferences.isBackgroundSyncEnabled()) { + if (unifiedPushHelper.isBackgroundSync()) { setCurrentNotificationStatus(session) } else { deleteCurrentNotificationStatus(session) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt index 3bca0da84e3..f82663f6e4e 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt @@ -17,6 +17,7 @@ package im.vector.app.features.settings.devices.v2.notification import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.FakeUnifiedPushHelper import im.vector.app.test.fakes.FakeVectorPreferences import io.mockk.coJustRun import io.mockk.coVerify @@ -30,12 +31,14 @@ import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent class UpdateNotificationSettingsAccountDataUseCaseTest { private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() private val fakeGetNotificationSettingsAccountDataUseCase = mockk() private val fakeSetNotificationSettingsAccountDataUseCase = mockk() private val fakeDeleteNotificationSettingsAccountDataUseCase = mockk() private val updateNotificationSettingsAccountDataUseCase = UpdateNotificationSettingsAccountDataUseCase( vectorPreferences = fakeVectorPreferences.instance, + unifiedPushHelper = fakeUnifiedPushHelper.instance, getNotificationSettingsAccountDataUseCase = fakeGetNotificationSettingsAccountDataUseCase, setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, deleteNotificationSettingsAccountDataUseCase = fakeDeleteNotificationSettingsAccountDataUseCase, @@ -50,7 +53,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest { coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } val areNotificationsEnabled = true fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled) - fakeVectorPreferences.givenIsBackgroundSyncEnabled(true) + fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(true) every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns LocalNotificationSettingsContent( isSilenced = null @@ -64,7 +67,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest { // Then verify { - fakeVectorPreferences.instance.isBackgroundSyncEnabled() + fakeUnifiedPushHelper.instance.isBackgroundSync() fakeVectorPreferences.instance.areNotificationEnabledForDevice() fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) } @@ -81,7 +84,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest { coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } val areNotificationsEnabled = true fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled) - fakeVectorPreferences.givenIsBackgroundSyncEnabled(true) + fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(true) every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns LocalNotificationSettingsContent( isSilenced = false @@ -95,7 +98,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest { // Then verify { - fakeVectorPreferences.instance.isBackgroundSyncEnabled() + fakeUnifiedPushHelper.instance.isBackgroundSync() fakeVectorPreferences.instance.areNotificationEnabledForDevice() fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) } @@ -110,14 +113,14 @@ class UpdateNotificationSettingsAccountDataUseCaseTest { val aSession = FakeSession() aSession.givenSessionId(aDeviceId) coJustRun { fakeDeleteNotificationSettingsAccountDataUseCase.execute(any()) } - fakeVectorPreferences.givenIsBackgroundSyncEnabled(false) + fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(false) // When updateNotificationSettingsAccountDataUseCase.execute(aSession) // Then verify { - fakeVectorPreferences.instance.isBackgroundSyncEnabled() + fakeUnifiedPushHelper.instance.isBackgroundSync() } coVerify { fakeDeleteNotificationSettingsAccountDataUseCase.execute(aSession) } coVerify(inverse = true) { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, any()) } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt index 99b5b758742..1a09783fadc 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt @@ -31,4 +31,8 @@ class FakeUnifiedPushHelper { fun givenGetEndpointOrTokenReturns(endpoint: String?) { every { instance.getEndpointOrToken() } returns endpoint } + + fun givenIsBackgroundSyncReturns(enabled: Boolean) { + every { instance.isBackgroundSync() } returns enabled + } } From 9dff4ff949672207112d29eab807e8f1c0a8d0d6 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 1 Dec 2022 10:30:09 +0100 Subject: [PATCH 093/197] Fixing import order after rebase --- .../vector/app/core/session/ConfigureAndStartSessionUseCase.kt | 2 +- .../app/core/session/ConfigureAndStartSessionUseCaseTest.kt | 2 +- .../test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt index d167b02d059..fbf89b76a48 100644 --- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt @@ -24,8 +24,8 @@ import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences -import im.vector.app.features.sync.SyncUtils import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase +import im.vector.app.features.sync.SyncUtils import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import timber.log.Timber diff --git a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt index 4071afaf3f9..3fb128c7598 100644 --- a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt @@ -19,8 +19,8 @@ package im.vector.app.core.session import im.vector.app.core.extensions.startSyncing import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase import im.vector.app.features.session.coroutineScope -import im.vector.app.features.sync.SyncUtils import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase +import im.vector.app.features.sync.SyncUtils import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeNotificationsSettingUpdater import im.vector.app.test.fakes.FakeSession diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index 94bc0966c56..3d7de662bd0 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -16,8 +16,8 @@ package im.vector.app.test.fakes -import im.vector.app.features.settings.BackgroundSyncMode import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences import io.mockk.every import io.mockk.justRun From 8973f3892a3fe10df8a784f6d3042e06453430e9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 1 Dec 2022 11:34:30 +0100 Subject: [PATCH 094/197] Fixing unit tests after rebase --- .../UpdateNotificationSettingsAccountDataUseCaseTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt index f82663f6e4e..0075be02d24 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt @@ -52,7 +52,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest { aSession.givenSessionId(aDeviceId) coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } val areNotificationsEnabled = true - fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled) + fakeVectorPreferences.givenAreNotificationsEnabledForDevice(areNotificationsEnabled) fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(true) every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns LocalNotificationSettingsContent( @@ -83,7 +83,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest { aSession.givenSessionId(aDeviceId) coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } val areNotificationsEnabled = true - fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled) + fakeVectorPreferences.givenAreNotificationsEnabledForDevice(areNotificationsEnabled) fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(true) every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns LocalNotificationSettingsContent( From 3f5147ddcef841caa40c56cd092916020bb06f21 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 1 Dec 2022 16:47:10 +0100 Subject: [PATCH 095/197] Fixing the toggle notifications use case for current session --- ...eCase.kt => ToggleNotificationsUseCase.kt} | 2 +- .../v2/overview/SessionOverviewViewModel.kt | 6 +- ...leNotificationsForCurrentSessionUseCase.kt | 9 +-- ...leNotificationsForCurrentSessionUseCase.kt | 10 +--- ...leNotificationsForCurrentSessionUseCase.kt | 57 +++++++++++++++++++ ...SettingsNotificationPreferenceViewModel.kt | 6 +- ...t.kt => ToggleNotificationsUseCaseTest.kt} | 10 ++-- ...tificationsForCurrentSessionUseCaseTest.kt | 14 ++--- ...tificationsForCurrentSessionUseCaseTest.kt | 8 +-- .../fakes/FakeToggleNotificationUseCase.kt | 4 +- 10 files changed, 92 insertions(+), 34 deletions(-) rename vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/{ToggleNotificationUseCase.kt => ToggleNotificationsUseCase.kt} (97%) create mode 100644 vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt rename vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/{ToggleNotificationUseCaseTest.kt => ToggleNotificationsUseCaseTest.kt} (93%) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCase.kt similarity index 97% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCase.kt index 73a81a6de17..77195ea950a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCase.kt @@ -20,7 +20,7 @@ import im.vector.app.core.di.ActiveSessionHolder import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent import javax.inject.Inject -class ToggleNotificationUseCase @Inject constructor( +class ToggleNotificationsUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, private val checkIfCanToggleNotificationsViaPusherUseCase: CheckIfCanToggleNotificationsViaPusherUseCase, private val checkIfCanToggleNotificationsViaAccountDataUseCase: CheckIfCanToggleNotificationsViaAccountDataUseCase, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index f598c397de6..55866cb8c41 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -31,7 +31,7 @@ import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase -import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase +import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationsUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase @@ -52,7 +52,7 @@ class SessionOverviewViewModel @AssistedInject constructor( private val signoutSessionsUseCase: SignoutSessionsUseCase, private val pendingAuthHandler: PendingAuthHandler, private val activeSessionHolder: ActiveSessionHolder, - private val toggleNotificationUseCase: ToggleNotificationUseCase, + private val toggleNotificationsUseCase: ToggleNotificationsUseCase, private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase, refreshDevicesUseCase: RefreshDevicesUseCase, private val vectorPreferences: VectorPreferences, @@ -226,7 +226,7 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) { viewModelScope.launch { - toggleNotificationUseCase.execute(action.deviceId, action.enabled) + toggleNotificationsUseCase.execute(action.deviceId, action.enabled) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt index 380c3b5e4e4..91731070187 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt @@ -20,21 +20,22 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase -import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase import javax.inject.Inject class DisableNotificationsForCurrentSessionUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, private val pushersManager: PushersManager, private val checkIfCanToggleNotificationsViaPusherUseCase: CheckIfCanToggleNotificationsViaPusherUseCase, - private val toggleNotificationUseCase: ToggleNotificationUseCase, + private val toggleNotificationsForCurrentSessionUseCase: ToggleNotificationsForCurrentSessionUseCase, private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, ) { + // TODO update unit tests suspend fun execute() { val session = activeSessionHolder.getSafeActiveSession() ?: return - val deviceId = session.sessionParams.deviceId ?: return - toggleNotificationUseCase.execute(deviceId, enabled = false) + toggleNotificationsForCurrentSessionUseCase.execute(enabled = false) + + // handle case when server does not support toggle of pusher if (!checkIfCanToggleNotificationsViaPusherUseCase.execute(session)) { unregisterUnifiedPushUseCase.execute(pushersManager) } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt index 89633a10c24..663c5004e86 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt @@ -16,17 +16,14 @@ package im.vector.app.features.settings.notifications -import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.RegisterUnifiedPushUseCase -import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase import javax.inject.Inject class EnableNotificationsForCurrentSessionUseCase @Inject constructor( - private val activeSessionHolder: ActiveSessionHolder, private val pushersManager: PushersManager, - private val toggleNotificationUseCase: ToggleNotificationUseCase, + private val toggleNotificationsForCurrentSessionUseCase: ToggleNotificationsForCurrentSessionUseCase, private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase, ) { @@ -37,6 +34,7 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor( object NeedToAskUserForDistributor : EnableNotificationsResult } + // TODO update unit tests suspend fun execute(distributor: String = ""): EnableNotificationsResult { val pusherForCurrentSession = pushersManager.getPusherForCurrentSession() if (pusherForCurrentSession == null) { @@ -50,9 +48,7 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor( } } - val session = activeSessionHolder.getSafeActiveSession() ?: return EnableNotificationsResult.Failure - val deviceId = session.sessionParams.deviceId ?: return EnableNotificationsResult.Failure - toggleNotificationUseCase.execute(deviceId, enabled = true) + toggleNotificationsForCurrentSessionUseCase.execute(enabled = true) return EnableNotificationsResult.Success } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt new file mode 100644 index 00000000000..64b4b1bb890 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.notifications + +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase +import im.vector.app.features.settings.devices.v2.notification.DeleteNotificationSettingsAccountDataUseCase +import im.vector.app.features.settings.devices.v2.notification.SetNotificationSettingsAccountDataUseCase +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import timber.log.Timber +import javax.inject.Inject + +class ToggleNotificationsForCurrentSessionUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val unifiedPushHelper: UnifiedPushHelper, + private val checkIfCanToggleNotificationsViaPusherUseCase: CheckIfCanToggleNotificationsViaPusherUseCase, + private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase, + private val deleteNotificationSettingsAccountDataUseCase: DeleteNotificationSettingsAccountDataUseCase, +) { + + // TODO add unit tests + suspend fun execute(enabled: Boolean) { + val session = activeSessionHolder.getSafeActiveSession() ?: return + val deviceId = session.sessionParams.deviceId ?: return + + if (unifiedPushHelper.isBackgroundSync()) { + Timber.d("background sync is enabled, setting account data event") + val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled) + setNotificationSettingsAccountDataUseCase.execute(session, deviceId, newNotificationSettingsContent) + } else { + Timber.d("push notif is enabled, deleting any account data and updating pusher") + deleteNotificationSettingsAccountDataUseCase.execute(session) + + if (checkIfCanToggleNotificationsViaPusherUseCase.execute(session)) { + val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } + devicePusher?.let { pusher -> + session.pushersService().togglePusher(pusher, enabled) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt index d6a9c621f2b..357f6458f27 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt @@ -40,6 +40,7 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor( private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase, + private val toggleNotificationsForCurrentSessionUseCase: ToggleNotificationsForCurrentSessionUseCase, ) : VectorViewModel(initialState) { @AssistedFactory @@ -80,6 +81,7 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor( } } + // TODO update unit tests private fun handleRegisterPushDistributor(distributor: String) { viewModelScope.launch { unregisterUnifiedPushUseCase.execute(pushersManager) @@ -88,7 +90,9 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor( _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor) } RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> { - ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice()) + val areNotificationsEnabled = vectorPreferences.areNotificationEnabledForDevice() + ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = areNotificationsEnabled) + toggleNotificationsForCurrentSessionUseCase.execute(enabled = areNotificationsEnabled) _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged) } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCaseTest.kt similarity index 93% rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCaseTest.kt index 1e3517c776e..90afbe90459 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCaseTest.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent -class ToggleNotificationUseCaseTest { +class ToggleNotificationsUseCaseTest { private val activeSessionHolder = FakeActiveSessionHolder() private val fakeCheckIfCanToggleNotificationsViaPusherUseCase = @@ -36,8 +36,8 @@ class ToggleNotificationUseCaseTest { private val fakeSetNotificationSettingsAccountDataUseCase = mockk() - private val toggleNotificationUseCase = - ToggleNotificationUseCase( + private val toggleNotificationsUseCase = + ToggleNotificationsUseCase( activeSessionHolder = activeSessionHolder.instance, checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase, checkIfCanToggleNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase, @@ -59,7 +59,7 @@ class ToggleNotificationUseCaseTest { every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns false // When - toggleNotificationUseCase.execute(sessionId, true) + toggleNotificationsUseCase.execute(sessionId, true) // Then activeSessionHolder.fakeSession.pushersService().verifyTogglePusherCalled(pushers.first(), true) @@ -78,7 +78,7 @@ class ToggleNotificationUseCaseTest { ) // When - toggleNotificationUseCase.execute(sessionId, true) + toggleNotificationsUseCase.execute(sessionId, true) // Then coVerify { diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt index e53874858a4..7e2a118e0e3 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt @@ -18,7 +18,7 @@ package im.vector.app.features.settings.notifications import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase -import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase +import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationsUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePushersManager import io.mockk.coJustRun @@ -35,14 +35,14 @@ class DisableNotificationsForCurrentSessionUseCaseTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakePushersManager = FakePushersManager() private val fakeCheckIfCanToggleNotificationsViaPusherUseCase = mockk() - private val fakeToggleNotificationUseCase = mockk() + private val fakeToggleNotificationsUseCase = mockk() private val fakeUnregisterUnifiedPushUseCase = mockk() private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase( activeSessionHolder = fakeActiveSessionHolder.instance, pushersManager = fakePushersManager.instance, checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase, - toggleNotificationUseCase = fakeToggleNotificationUseCase, + toggleNotificationUseCase = fakeToggleNotificationsUseCase, unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase, ) @@ -52,13 +52,13 @@ class DisableNotificationsForCurrentSessionUseCaseTest { val fakeSession = fakeActiveSessionHolder.fakeSession fakeSession.givenSessionId(A_SESSION_ID) every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns true - coJustRun { fakeToggleNotificationUseCase.execute(A_SESSION_ID, any()) } + coJustRun { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, any()) } // When disableNotificationsForCurrentSessionUseCase.execute() // Then - coVerify { fakeToggleNotificationUseCase.execute(A_SESSION_ID, false) } + coVerify { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, false) } } @Test @@ -67,7 +67,7 @@ class DisableNotificationsForCurrentSessionUseCaseTest { val fakeSession = fakeActiveSessionHolder.fakeSession fakeSession.givenSessionId(A_SESSION_ID) every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns false - coJustRun { fakeToggleNotificationUseCase.execute(A_SESSION_ID, any()) } + coJustRun { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, any()) } coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) } // When @@ -75,7 +75,7 @@ class DisableNotificationsForCurrentSessionUseCaseTest { // Then coVerify { - fakeToggleNotificationUseCase.execute(A_SESSION_ID, false) + fakeToggleNotificationsUseCase.execute(A_SESSION_ID, false) fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance) } } diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt index f10b0777cbe..7beab170f21 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt @@ -18,7 +18,7 @@ package im.vector.app.features.settings.notifications import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase import im.vector.app.core.pushers.RegisterUnifiedPushUseCase -import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase +import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationsUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePushersManager import io.mockk.coJustRun @@ -36,14 +36,14 @@ class EnableNotificationsForCurrentSessionUseCaseTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakePushersManager = FakePushersManager() - private val fakeToggleNotificationUseCase = mockk() + private val fakeToggleNotificationsUseCase = mockk() private val fakeRegisterUnifiedPushUseCase = mockk() private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk() private val enableNotificationsForCurrentSessionUseCase = EnableNotificationsForCurrentSessionUseCase( activeSessionHolder = fakeActiveSessionHolder.instance, pushersManager = fakePushersManager.instance, - toggleNotificationUseCase = fakeToggleNotificationUseCase, + toggleNotificationUseCase = fakeToggleNotificationsUseCase, registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase, ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase, ) @@ -57,7 +57,7 @@ class EnableNotificationsForCurrentSessionUseCaseTest { fakePushersManager.givenGetPusherForCurrentSessionReturns(null) every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) } - coJustRun { fakeToggleNotificationUseCase.execute(A_SESSION_ID, any()) } + coJustRun { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, any()) } // When val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor) diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt index 527625144e0..3d2179bc2df 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt @@ -16,14 +16,14 @@ package im.vector.app.test.fakes -import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase +import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationsUseCase import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.mockk class FakeToggleNotificationUseCase { - val instance = mockk { + val instance = mockk { coJustRun { execute(any(), any()) } } From 06681fd115596ee9b14a30aef94f73d3a62de79b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 1 Dec 2022 17:12:09 +0100 Subject: [PATCH 096/197] Removing listening on background sync preference --- .../NotificationsSettingUpdater.kt | 27 ------------ .../NotificationsSettingUpdaterTest.kt | 41 ------------------- 2 files changed, 68 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt index 4a16f37cfeb..a4d18baa646 100644 --- a/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt +++ b/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt @@ -16,11 +16,7 @@ package im.vector.app.core.notification -import android.content.SharedPreferences.OnSharedPreferenceChangeListener import im.vector.app.features.session.coroutineScope -import im.vector.app.features.settings.VectorPreferences -import im.vector.app.features.settings.VectorPreferences.Companion.SETTINGS_FDROID_BACKGROUND_SYNC_MODE -import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session @@ -29,21 +25,16 @@ import javax.inject.Singleton /** * Listen changes in Pusher or Account Data to update the local setting for notification toggle. - * Listen changes on background sync mode preference to update the corresponding Account Data event. */ @Singleton class NotificationsSettingUpdater @Inject constructor( private val updateEnableNotificationsSettingOnChangeUseCase: UpdateEnableNotificationsSettingOnChangeUseCase, - private val vectorPreferences: VectorPreferences, - private val updateNotificationSettingsAccountDataUseCase: UpdateNotificationSettingsAccountDataUseCase, ) { private var job: Job? = null - private var prefChangeListener: OnSharedPreferenceChangeListener? = null fun onSessionStarted(session: Session) { updateEnableNotificationsSettingOnChange(session) - updateAccountDataOnBackgroundSyncChange(session) } private fun updateEnableNotificationsSettingOnChange(session: Session) { @@ -52,22 +43,4 @@ class NotificationsSettingUpdater @Inject constructor( updateEnableNotificationsSettingOnChangeUseCase.execute(session) } } - - private fun updateAccountDataOnBackgroundSyncChange(session: Session) { - prefChangeListener?.let { vectorPreferences.unsubscribeToChanges(it) } - prefChangeListener = null - prefChangeListener = createPrefListener(session).also { - vectorPreferences.subscribeToChanges(it) - } - } - - private fun createPrefListener(session: Session): OnSharedPreferenceChangeListener { - return OnSharedPreferenceChangeListener { _, key -> - session.coroutineScope.launch { - if (key == SETTINGS_FDROID_BACKGROUND_SYNC_MODE) { - updateNotificationSettingsAccountDataUseCase.execute(session) - } - } - } - } } diff --git a/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt b/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt index 386b52e61e6..0920ee4716e 100644 --- a/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt +++ b/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt @@ -17,10 +17,7 @@ package im.vector.app.core.notification import im.vector.app.features.session.coroutineScope -import im.vector.app.features.settings.VectorPreferences -import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase import im.vector.app.test.fakes.FakeSession -import im.vector.app.test.fakes.FakeVectorPreferences import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.every @@ -36,13 +33,9 @@ import org.junit.Test class NotificationsSettingUpdaterTest { private val fakeUpdateEnableNotificationsSettingOnChangeUseCase = mockk() - private val fakeVectorPreferences = FakeVectorPreferences() - private val fakeUpdateNotificationSettingsAccountDataUseCase = mockk() private val notificationsSettingUpdater = NotificationsSettingUpdater( updateEnableNotificationsSettingOnChangeUseCase = fakeUpdateEnableNotificationsSettingOnChangeUseCase, - vectorPreferences = fakeVectorPreferences.instance, - updateNotificationSettingsAccountDataUseCase = fakeUpdateNotificationSettingsAccountDataUseCase, ) @Before @@ -69,38 +62,4 @@ class NotificationsSettingUpdaterTest { // Then coVerify { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(aSession) } } - - @Test - fun `given a session when calling onSessionStarted then update account data on background sync preference change`() = runTest { - // Given - val aSession = FakeSession() - every { aSession.coroutineScope } returns this - coJustRun { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(any()) } - coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) } - fakeVectorPreferences.givenChangeOnPreference(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE) - - // When - notificationsSettingUpdater.onSessionStarted(aSession) - advanceUntilIdle() - - // Then - coVerify { fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) } - } - - @Test - fun `given a session when calling onSessionStarted then account data is not updated on other preference change`() = runTest { - // Given - val aSession = FakeSession() - every { aSession.coroutineScope } returns this - coJustRun { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(any()) } - coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) } - fakeVectorPreferences.givenChangeOnPreference("key") - - // When - notificationsSettingUpdater.onSessionStarted(aSession) - advanceUntilIdle() - - // Then - coVerify(inverse = true) { fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) } - } } From 5248a69fe2bd498a892f5484fff96b60ccec0154 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 1 Dec 2022 17:33:27 +0100 Subject: [PATCH 097/197] Updating existing unit tests --- ...leNotificationsForCurrentSessionUseCase.kt | 1 - ...leNotificationsForCurrentSessionUseCase.kt | 2 -- ...rSettingsNotificationPreferenceFragment.kt | 1 - ...SettingsNotificationPreferenceViewEvent.kt | 1 - ...SettingsNotificationPreferenceViewModel.kt | 4 --- .../overview/SessionOverviewViewModelTest.kt | 2 +- ...tificationsForCurrentSessionUseCaseTest.kt | 20 +++++------ ...tificationsForCurrentSessionUseCaseTest.kt | 34 ++++--------------- ...ingsNotificationPreferenceViewModelTest.kt | 27 +++------------ 9 files changed, 21 insertions(+), 71 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt index 91731070187..daa58578d6f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt @@ -30,7 +30,6 @@ class DisableNotificationsForCurrentSessionUseCase @Inject constructor( private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, ) { - // TODO update unit tests suspend fun execute() { val session = activeSessionHolder.getSafeActiveSession() ?: return toggleNotificationsForCurrentSessionUseCase.execute(enabled = false) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt index 663c5004e86..daf3890e336 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt @@ -30,11 +30,9 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor( sealed interface EnableNotificationsResult { object Success : EnableNotificationsResult - object Failure : EnableNotificationsResult object NeedToAskUserForDistributor : EnableNotificationsResult } - // TODO update unit tests suspend fun execute(distributor: String = ""): EnableNotificationsResult { val pusherForCurrentSession = pushersManager.getPusherForCurrentSession() if (pusherForCurrentSession == null) { diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 238ed4218ce..490a47ef618 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -119,7 +119,6 @@ class VectorSettingsNotificationPreferenceFragment : VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled -> onNotificationsForDeviceEnabled() VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled -> onNotificationsForDeviceDisabled() is VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor -> askUserToSelectPushDistributor() - VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure -> displayErrorDialog(throwable = null) VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged -> onNotificationMethodChanged() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt index e4cf8e19733..b0ee107769e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt @@ -20,7 +20,6 @@ import im.vector.app.core.platform.VectorViewEvents sealed interface VectorSettingsNotificationPreferenceViewEvent : VectorViewEvents { object NotificationsForDeviceEnabled : VectorSettingsNotificationPreferenceViewEvent - object EnableNotificationForDeviceFailure : VectorSettingsNotificationPreferenceViewEvent object NotificationsForDeviceDisabled : VectorSettingsNotificationPreferenceViewEvent object AskUserForPushDistributor : VectorSettingsNotificationPreferenceViewEvent object NotificationMethodChanged : VectorSettingsNotificationPreferenceViewEvent diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt index 357f6458f27..48e82b35e85 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt @@ -68,9 +68,6 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor( private fun handleEnableNotificationsForDevice(distributor: String) { viewModelScope.launch { when (enableNotificationsForCurrentSessionUseCase.execute(distributor)) { - EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure -> { - _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure) - } is EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor -> { _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor) } @@ -81,7 +78,6 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor( } } - // TODO update unit tests private fun handleRegisterPushDistributor(distributor: String) { viewModelScope.launch { unregisterUnifiedPushUseCase.execute(pushersManager) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 6018152176f..b0f7a774f2c 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -88,7 +88,7 @@ class SessionOverviewViewModelTest { pendingAuthHandler = fakePendingAuthHandler.instance, activeSessionHolder = fakeActiveSessionHolder.instance, refreshDevicesUseCase = refreshDevicesUseCase, - toggleNotificationUseCase = toggleNotificationUseCase.instance, + toggleNotificationsUseCase = toggleNotificationUseCase.instance, getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance, vectorPreferences = fakeVectorPreferences.instance, toggleIpAddressVisibilityUseCase = toggleIpAddressVisibilityUseCase, diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt index 7e2a118e0e3..b7749d0252f 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt @@ -18,7 +18,6 @@ package im.vector.app.features.settings.notifications import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase -import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationsUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePushersManager import io.mockk.coJustRun @@ -28,21 +27,19 @@ import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Test -private const val A_SESSION_ID = "session-id" - class DisableNotificationsForCurrentSessionUseCaseTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakePushersManager = FakePushersManager() private val fakeCheckIfCanToggleNotificationsViaPusherUseCase = mockk() - private val fakeToggleNotificationsUseCase = mockk() + private val fakeToggleNotificationsForCurrentSessionUseCase = mockk() private val fakeUnregisterUnifiedPushUseCase = mockk() private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase( activeSessionHolder = fakeActiveSessionHolder.instance, pushersManager = fakePushersManager.instance, checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase, - toggleNotificationUseCase = fakeToggleNotificationsUseCase, + toggleNotificationsForCurrentSessionUseCase = fakeToggleNotificationsForCurrentSessionUseCase, unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase, ) @@ -50,24 +47,25 @@ class DisableNotificationsForCurrentSessionUseCaseTest { fun `given toggle via pusher is possible when execute then disable notification via toggle of existing pusher`() = runTest { // Given val fakeSession = fakeActiveSessionHolder.fakeSession - fakeSession.givenSessionId(A_SESSION_ID) every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns true - coJustRun { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, any()) } + coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) } // When disableNotificationsForCurrentSessionUseCase.execute() // Then - coVerify { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, false) } + coVerify { fakeToggleNotificationsForCurrentSessionUseCase.execute(false) } + coVerify(inverse = true) { + fakeUnregisterUnifiedPushUseCase.execute(any()) + } } @Test fun `given toggle via pusher is NOT possible when execute then disable notification by unregistering the pusher`() = runTest { // Given val fakeSession = fakeActiveSessionHolder.fakeSession - fakeSession.givenSessionId(A_SESSION_ID) every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns false - coJustRun { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, any()) } + coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) } coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) } // When @@ -75,7 +73,7 @@ class DisableNotificationsForCurrentSessionUseCaseTest { // Then coVerify { - fakeToggleNotificationsUseCase.execute(A_SESSION_ID, false) + fakeToggleNotificationsForCurrentSessionUseCase.execute(false) fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance) } } diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt index 7beab170f21..d58ba7645cb 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt @@ -18,10 +18,9 @@ package im.vector.app.features.settings.notifications import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase import im.vector.app.core.pushers.RegisterUnifiedPushUseCase -import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationsUseCase -import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePushersManager import io.mockk.coJustRun +import io.mockk.coVerify import io.mockk.every import io.mockk.justRun import io.mockk.mockk @@ -30,20 +29,16 @@ import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBe import org.junit.Test -private const val A_SESSION_ID = "session-id" - class EnableNotificationsForCurrentSessionUseCaseTest { - private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakePushersManager = FakePushersManager() - private val fakeToggleNotificationsUseCase = mockk() + private val fakeToggleNotificationsForCurrentSessionUseCase = mockk() private val fakeRegisterUnifiedPushUseCase = mockk() private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk() private val enableNotificationsForCurrentSessionUseCase = EnableNotificationsForCurrentSessionUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance, pushersManager = fakePushersManager.instance, - toggleNotificationUseCase = fakeToggleNotificationsUseCase, + toggleNotificationsForCurrentSessionUseCase = fakeToggleNotificationsForCurrentSessionUseCase, registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase, ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase, ) @@ -52,12 +47,10 @@ class EnableNotificationsForCurrentSessionUseCaseTest { fun `given no existing pusher and a registered distributor when execute then a new pusher is registered and result is success`() = runTest { // Given val aDistributor = "distributor" - val fakeSession = fakeActiveSessionHolder.fakeSession - fakeSession.givenSessionId(A_SESSION_ID) fakePushersManager.givenGetPusherForCurrentSessionReturns(null) every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) } - coJustRun { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, any()) } + coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) } // When val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor) @@ -68,6 +61,9 @@ class EnableNotificationsForCurrentSessionUseCaseTest { fakeRegisterUnifiedPushUseCase.execute(aDistributor) fakeEnsureFcmTokenIsRetrievedUseCase.execute(fakePushersManager.instance, registerPusher = true) } + coVerify { + fakeToggleNotificationsForCurrentSessionUseCase.execute(enabled = true) + } } @Test @@ -86,20 +82,4 @@ class EnableNotificationsForCurrentSessionUseCaseTest { fakeRegisterUnifiedPushUseCase.execute(aDistributor) } } - - @Test - fun `given no deviceId for current session when execute then result is failure`() = runTest { - // Given - val aDistributor = "distributor" - val fakeSession = fakeActiveSessionHolder.fakeSession - fakeSession.givenSessionId(null) - fakePushersManager.givenGetPusherForCurrentSessionReturns(mockk()) - every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor - - // When - val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor) - - // Then - result shouldBe EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure - } } diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt index f9d75273164..270447c461f 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt @@ -46,6 +46,7 @@ class VectorSettingsNotificationPreferenceViewModelTest { private val fakeUnregisterUnifiedPushUseCase = mockk() private val fakeRegisterUnifiedPushUseCase = mockk() private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk() + private val fakeToggleNotificationsForCurrentSessionUseCase = mockk() private fun createViewModel() = VectorSettingsNotificationPreferenceViewModel( initialState = VectorDummyViewState(), @@ -56,6 +57,7 @@ class VectorSettingsNotificationPreferenceViewModelTest { unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase, registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase, ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase, + toggleNotificationsForCurrentSessionUseCase = fakeToggleNotificationsForCurrentSessionUseCase, ) @Test @@ -125,29 +127,6 @@ class VectorSettingsNotificationPreferenceViewModelTest { } } - @Test - fun `given EnableNotificationsForDevice action and enable failure when handling action then enable use case is called`() { - // Given - val viewModel = createViewModel() - val aDistributor = "aDistributor" - val action = VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(aDistributor) - coEvery { fakeEnableNotificationsForCurrentSessionUseCase.execute(any()) } returns - EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure - val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure - - // When - val viewModelTest = viewModel.test() - viewModel.handle(action) - - // Then - viewModelTest - .assertEvent { event -> event == expectedEvent } - .finish() - coVerify { - fakeEnableNotificationsForCurrentSessionUseCase.execute(aDistributor) - } - } - @Test fun `given RegisterPushDistributor action and register success when handling action then register use case is called`() { // Given @@ -158,6 +137,7 @@ class VectorSettingsNotificationPreferenceViewModelTest { coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) } val areNotificationsEnabled = true fakeVectorPreferences.givenAreNotificationsEnabledForDevice(areNotificationsEnabled) + coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) } justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) } val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged @@ -173,6 +153,7 @@ class VectorSettingsNotificationPreferenceViewModelTest { fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance) fakeRegisterUnifiedPushUseCase.execute(aDistributor) fakeEnsureFcmTokenIsRetrievedUseCase.execute(fakePushersManager.instance, registerPusher = areNotificationsEnabled) + fakeToggleNotificationsForCurrentSessionUseCase.execute(enabled = areNotificationsEnabled) } } From b78de152286cc75f350a444863e6625d79144752 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 1 Dec 2022 17:55:30 +0100 Subject: [PATCH 098/197] Adding unit tests for new toggle notification for current session use case --- ...leNotificationsForCurrentSessionUseCase.kt | 1 - ...tificationsForCurrentSessionUseCaseTest.kt | 117 ++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 vector/src/test/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt index 64b4b1bb890..3dc73f0a31e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt @@ -33,7 +33,6 @@ class ToggleNotificationsForCurrentSessionUseCase @Inject constructor( private val deleteNotificationSettingsAccountDataUseCase: DeleteNotificationSettingsAccountDataUseCase, ) { - // TODO add unit tests suspend fun execute(enabled: Boolean) { val session = activeSessionHolder.getSafeActiveSession() ?: return val deviceId = session.sessionParams.deviceId ?: return diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCaseTest.kt new file mode 100644 index 00000000000..f49aafab8ad --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCaseTest.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.notifications + +import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase +import im.vector.app.features.settings.devices.v2.notification.DeleteNotificationSettingsAccountDataUseCase +import im.vector.app.features.settings.devices.v2.notification.SetNotificationSettingsAccountDataUseCase +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeUnifiedPushHelper +import im.vector.app.test.fixtures.PusherFixture +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent + +class ToggleNotificationsForCurrentSessionUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() + private val fakeCheckIfCanToggleNotificationsViaPusherUseCase = mockk() + private val fakeSetNotificationSettingsAccountDataUseCase = mockk() + private val fakeDeleteNotificationSettingsAccountDataUseCase = mockk() + + private val toggleNotificationsForCurrentSessionUseCase = ToggleNotificationsForCurrentSessionUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + unifiedPushHelper = fakeUnifiedPushHelper.instance, + checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase, + setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, + deleteNotificationSettingsAccountDataUseCase = fakeDeleteNotificationSettingsAccountDataUseCase, + ) + + @Test + fun `given background sync is enabled when execute then set the related account data with correct value`() = runTest { + // Given + val enabled = true + val aDeviceId = "deviceId" + fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(true) + fakeActiveSessionHolder.fakeSession.givenSessionId(aDeviceId) + coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } + val expectedNotificationContent = LocalNotificationSettingsContent(isSilenced = !enabled) + + // When + toggleNotificationsForCurrentSessionUseCase.execute(enabled) + + // Then + coVerify { + fakeSetNotificationSettingsAccountDataUseCase.execute( + fakeActiveSessionHolder.fakeSession, + aDeviceId, + expectedNotificationContent + ) + } + } + + @Test + fun `given background sync is not enabled and toggle pusher is possible when execute then delete any related account data and toggle pusher`() = runTest { + // Given + val enabled = true + val aDeviceId = "deviceId" + fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(false) + fakeActiveSessionHolder.fakeSession.givenSessionId(aDeviceId) + coJustRun { fakeDeleteNotificationSettingsAccountDataUseCase.execute(any()) } + every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(any()) } returns true + val aPusher = PusherFixture.aPusher(deviceId = aDeviceId) + fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(aPusher)) + + // When + toggleNotificationsForCurrentSessionUseCase.execute(enabled) + + // Then + coVerify { + fakeDeleteNotificationSettingsAccountDataUseCase.execute(fakeActiveSessionHolder.fakeSession) + fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) + } + fakeActiveSessionHolder.fakeSession.fakePushersService.verifyTogglePusherCalled(aPusher, enabled) + } + + @Test + fun `given background sync is not enabled and toggle pusher is not possible when execute then only delete any related account data`() = runTest { + // Given + val enabled = true + val aDeviceId = "deviceId" + fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(false) + fakeActiveSessionHolder.fakeSession.givenSessionId(aDeviceId) + coJustRun { fakeDeleteNotificationSettingsAccountDataUseCase.execute(any()) } + every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(any()) } returns false + + // When + toggleNotificationsForCurrentSessionUseCase.execute(enabled) + + // Then + coVerify { + fakeDeleteNotificationSettingsAccountDataUseCase.execute(fakeActiveSessionHolder.fakeSession) + fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) + } + coVerify(inverse = true) { + fakeActiveSessionHolder.fakeSession.fakePushersService.togglePusher(any(), any()) + } + } +} From e09b9a2ce0e245e1311374135807c835bf14e419 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 2 Dec 2022 10:03:37 +0100 Subject: [PATCH 099/197] Fixing wrong notification status when no registered pusher for the session --- .../notification/GetNotificationsStatusUseCase.kt | 8 +++++++- .../GetNotificationsStatusUseCaseTest.kt | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index f98fd63efba..ae7e8595736 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -51,7 +51,13 @@ class GetNotificationsStatusUseCase @Inject constructor( .livePushers() .map { it.filter { pusher -> pusher.deviceId == deviceId } } .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } - .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + .map { + when (it) { + true -> NotificationsStatus.ENABLED + false -> NotificationsStatus.DISABLED + else -> NotificationsStatus.NOT_SUPPORTED + } + } .distinctUntilChanged() } else { flowOf(NotificationsStatus.NOT_SUPPORTED) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt index d4c3aa57889..3c454f79650 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt @@ -105,6 +105,20 @@ class GetNotificationsStatusUseCaseTest { result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED } + @Test + fun `given toggle via pusher is supported and no registered pusher when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { + // Given + fakeSession.pushersService().givenPushersLive(emptyList()) + every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false + every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true) + + // When + val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID) + + // Then + result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED + } + @Test fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on settings value`() = runTest { // Given From 635f975b6cb5bbb8170a982e4b713e432861e9d4 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 2 Dec 2022 15:13:10 +0100 Subject: [PATCH 100/197] Fix missing unregister of pusher when notifications are disabled --- .../features/home/HomeActivityViewModel.kt | 10 +++++++ ...leNotificationsForCurrentSessionUseCase.kt | 11 +------- ...tificationsForCurrentSessionUseCaseTest.kt | 28 +------------------ 3 files changed, 12 insertions(+), 37 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 26034fc09cb..a54ce2cff39 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -29,6 +29,7 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.RegisterUnifiedPushUseCase +import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsType @@ -92,6 +93,7 @@ class HomeActivityViewModel @AssistedInject constructor( private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase, private val pushersManager: PushersManager, private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, + private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase, ) : VectorViewModel(initialState) { @@ -130,6 +132,8 @@ class HomeActivityViewModel @AssistedInject constructor( private fun registerUnifiedPushIfNeeded() { if (vectorPreferences.areNotificationEnabledForDevice()) { registerUnifiedPush(distributor = "") + } else { + unregisterUnifiedPush() } } @@ -146,6 +150,12 @@ class HomeActivityViewModel @AssistedInject constructor( } } + private fun unregisterUnifiedPush() { + viewModelScope.launch { + unregisterUnifiedPushUseCase.execute(pushersManager) + } + } + private fun observeReleaseNotes() = withState { state -> if (vectorPreferences.isNewAppLayoutEnabled()) { // we don't want to show release notes for new users or after relogin diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt index daa58578d6f..0c50a296f36 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt @@ -16,27 +16,18 @@ package im.vector.app.features.settings.notifications -import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase -import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase import javax.inject.Inject class DisableNotificationsForCurrentSessionUseCase @Inject constructor( - private val activeSessionHolder: ActiveSessionHolder, private val pushersManager: PushersManager, - private val checkIfCanToggleNotificationsViaPusherUseCase: CheckIfCanToggleNotificationsViaPusherUseCase, private val toggleNotificationsForCurrentSessionUseCase: ToggleNotificationsForCurrentSessionUseCase, private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, ) { suspend fun execute() { - val session = activeSessionHolder.getSafeActiveSession() ?: return toggleNotificationsForCurrentSessionUseCase.execute(enabled = false) - - // handle case when server does not support toggle of pusher - if (!checkIfCanToggleNotificationsViaPusherUseCase.execute(session)) { - unregisterUnifiedPushUseCase.execute(pushersManager) - } + unregisterUnifiedPushUseCase.execute(pushersManager) } } diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt index b7749d0252f..669b20fc1a3 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt @@ -17,54 +17,28 @@ package im.vector.app.features.settings.notifications import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase -import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase -import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePushersManager import io.mockk.coJustRun import io.mockk.coVerify -import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Test class DisableNotificationsForCurrentSessionUseCaseTest { - private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakePushersManager = FakePushersManager() - private val fakeCheckIfCanToggleNotificationsViaPusherUseCase = mockk() private val fakeToggleNotificationsForCurrentSessionUseCase = mockk() private val fakeUnregisterUnifiedPushUseCase = mockk() private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance, pushersManager = fakePushersManager.instance, - checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase, toggleNotificationsForCurrentSessionUseCase = fakeToggleNotificationsForCurrentSessionUseCase, unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase, ) @Test - fun `given toggle via pusher is possible when execute then disable notification via toggle of existing pusher`() = runTest { + fun `when execute then disable notifications and unregister the pusher`() = runTest { // Given - val fakeSession = fakeActiveSessionHolder.fakeSession - every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns true - coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) } - - // When - disableNotificationsForCurrentSessionUseCase.execute() - - // Then - coVerify { fakeToggleNotificationsForCurrentSessionUseCase.execute(false) } - coVerify(inverse = true) { - fakeUnregisterUnifiedPushUseCase.execute(any()) - } - } - - @Test - fun `given toggle via pusher is NOT possible when execute then disable notification by unregistering the pusher`() = runTest { - // Given - val fakeSession = fakeActiveSessionHolder.fakeSession - every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns false coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) } coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) } From 18ab8a1279c94058b55d127df3e6985c2b5014fa Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 23 Nov 2022 17:22:34 +0100 Subject: [PATCH 101/197] Adding changelog entry --- changelog.d/7632.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7632.feature diff --git a/changelog.d/7632.feature b/changelog.d/7632.feature new file mode 100644 index 00000000000..460f9877567 --- /dev/null +++ b/changelog.d/7632.feature @@ -0,0 +1 @@ +Update notifications setting when m.local_notification_settings. event changes for current device From 9fbfe82044ec6792a114856a0bf408d40123f290 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 24 Nov 2022 16:56:06 +0100 Subject: [PATCH 102/197] Fix observation of the notification status for the current session --- ...oggleNotificationsViaAccountDataUseCase.kt | 32 +++++ ...icationSettingsAccountDataAsFlowUseCase.kt | 37 ++++++ .../GetNotificationsStatusUseCase.kt | 34 +++--- ...eNotificationsViaAccountDataUseCaseTest.kt | 88 ++++++++++++++ ...ionSettingsAccountDataAsFlowUseCaseTest.kt | 109 ++++++++++++++++++ .../GetNotificationsStatusUseCaseTest.kt | 25 ++-- .../fakes/FakeSessionAccountDataService.kt | 11 ++ 7 files changed, 314 insertions(+), 22 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCase.kt create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt new file mode 100644 index 00000000000..ac466852ebc --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject + +class CanToggleNotificationsViaAccountDataUseCase @Inject constructor( + private val getNotificationSettingsAccountDataAsFlowUseCase: GetNotificationSettingsAccountDataAsFlowUseCase, +) { + + fun execute(session: Session, deviceId: String): Flow { + return getNotificationSettingsAccountDataAsFlowUseCase.execute(session, deviceId) + .map { it?.isSilenced != null } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCase.kt new file mode 100644 index 00000000000..ea4bd40f1f2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCase.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import androidx.lifecycle.asFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toModel +import javax.inject.Inject + +class GetNotificationSettingsAccountDataAsFlowUseCase @Inject constructor() { + + fun execute(session: Session, deviceId: String): Flow { + return session + .accountDataService() + .getLiveUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) + .asFlow() + .map { it.getOrNull()?.content?.toModel() } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index ae7e8595736..8cf684975e8 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -31,20 +31,30 @@ import javax.inject.Inject class GetNotificationsStatusUseCase @Inject constructor( private val canToggleNotificationsViaPusherUseCase: CanToggleNotificationsViaPusherUseCase, - private val checkIfCanToggleNotificationsViaAccountDataUseCase: CheckIfCanToggleNotificationsViaAccountDataUseCase, + private val canToggleNotificationsViaAccountDataUseCase: CanToggleNotificationsViaAccountDataUseCase, ) { fun execute(session: Session, deviceId: String): Flow { - return when { - checkIfCanToggleNotificationsViaAccountDataUseCase.execute(session, deviceId) -> { - session.flow() - .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) - .unwrap() - .map { it.content.toModel()?.isSilenced?.not() } - .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } - .distinctUntilChanged() - } - else -> canToggleNotificationsViaPusherUseCase.execute(session) + return canToggleNotificationsViaAccountDataUseCase.execute(session, deviceId) + .flatMapLatest { canToggle -> + if (canToggle) { + notificationStatusFromAccountData(session, deviceId) + } else { + notificationStatusFromPusher(session, deviceId) + } + } + .distinctUntilChanged() + } + + private fun notificationStatusFromAccountData(session: Session, deviceId: String) = + session.flow() + .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) + .unwrap() + .map { it.content.toModel()?.isSilenced?.not() } + .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + + private fun notificationStatusFromPusher(session: Session, deviceId: String) = + canToggleNotificationsViaPusherUseCase.execute(session) .flatMapLatest { canToggle -> if (canToggle) { session.flow() @@ -63,6 +73,4 @@ class GetNotificationsStatusUseCase @Inject constructor( flowOf(NotificationsStatus.NOT_SUPPORTED) } } - } - } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt new file mode 100644 index 00000000000..a1dfed69029 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeSession +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent + +class CanToggleNotificationsViaAccountDataUseCaseTest { + + private val fakeGetNotificationSettingsAccountDataAsFlowUseCase = mockk() + + private val canToggleNotificationsViaAccountDataUseCase = CanToggleNotificationsViaAccountDataUseCase( + getNotificationSettingsAccountDataAsFlowUseCase = fakeGetNotificationSettingsAccountDataAsFlowUseCase, + ) + + @Test + fun `given current session and content for account data when execute then true is returned`() = runTest { + // Given + val aSession = FakeSession() + val aDeviceId = "aDeviceId" + val localNotificationSettingsContent = LocalNotificationSettingsContent( + isSilenced = true, + ) + every { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(any(), any()) } returns flowOf(localNotificationSettingsContent) + + // When + val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull() + + // Then + result shouldBe true + verify { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId) } + } + + @Test + fun `given current session and empty content for account data when execute then false is returned`() = runTest { + // Given + val aSession = FakeSession() + val aDeviceId = "aDeviceId" + val localNotificationSettingsContent = LocalNotificationSettingsContent( + isSilenced = null, + ) + every { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(any(), any()) } returns flowOf(localNotificationSettingsContent) + + // When + val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull() + + // Then + result shouldBe false + verify { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId) } + } + + @Test + fun `given current session and no related account data when execute then false is returned`() = runTest { + // Given + val aSession = FakeSession() + val aDeviceId = "aDeviceId" + every { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(any(), any()) } returns flowOf(null) + + // When + val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull() + + // Then + result shouldBe false + verify { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId) } + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt new file mode 100644 index 00000000000..6280d4c48b4 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeFlowLiveDataConversions +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.givenAsFlow +import io.mockk.unmockkAll +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toContent + +class GetNotificationSettingsAccountDataAsFlowUseCaseTest { + + private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions() + private val getNotificationSettingsAccountDataAsFlowUseCase = GetNotificationSettingsAccountDataAsFlowUseCase() + + @Before + fun setUp() { + fakeFlowLiveDataConversions.setup() + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a device id when execute then retrieve the account data event corresponding to this id if any`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + val expectedContent = LocalNotificationSettingsContent(isSilenced = true) + aSession + .accountDataService() + .givenGetLiveUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId, + content = expectedContent.toContent(), + ) + .givenAsFlow() + + // When + val result = getNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId).firstOrNull() + + // Then + result shouldBeEqualTo expectedContent + } + + @Test + fun `given a device id and no content for account data when execute then retrieve the account data event corresponding to this id if any`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + aSession + .accountDataService() + .givenGetLiveUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId, + content = null, + ) + .givenAsFlow() + + // When + val result = getNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId).firstOrNull() + + // Then + result shouldBeEqualTo null + } + + @Test + fun `given a device id and empty content for account data when execute then retrieve the account data event corresponding to this id if any`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + val expectedContent = LocalNotificationSettingsContent(isSilenced = null) + aSession + .accountDataService() + .givenGetLiveUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId, + content = expectedContent.toContent(), + ) + .givenAsFlow() + + // When + val result = getNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId).firstOrNull() + + // Then + result shouldBeEqualTo expectedContent + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt index 3c454f79650..e4b681c5ec5 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt @@ -22,6 +22,7 @@ import im.vector.app.test.fixtures.PusherFixture import im.vector.app.test.testDispatcher import io.mockk.every import io.mockk.mockk +import io.mockk.verify import io.mockk.verifyOrder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull @@ -46,14 +47,14 @@ class GetNotificationsStatusUseCaseTest { val instantTaskExecutorRule = InstantTaskExecutorRule() private val fakeSession = FakeSession() - private val fakeCheckIfCanToggleNotificationsViaAccountDataUseCase = - mockk() + private val fakeCanToggleNotificationsViaAccountDataUseCase = + mockk() private val fakeCanToggleNotificationsViaPusherUseCase = mockk() private val getNotificationsStatusUseCase = GetNotificationsStatusUseCase( - checkIfCanToggleNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase, + canToggleNotificationsViaAccountDataUseCase = fakeCanToggleNotificationsViaAccountDataUseCase, canToggleNotificationsViaPusherUseCase = fakeCanToggleNotificationsViaPusherUseCase, ) @@ -70,7 +71,7 @@ class GetNotificationsStatusUseCaseTest { @Test fun `given current session and toggle is not supported when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { // Given - every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false + every { fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns flowOf(false) every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false) // When @@ -80,7 +81,7 @@ class GetNotificationsStatusUseCaseTest { result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED verifyOrder { // we should first check account data - fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) + fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } } @@ -95,7 +96,7 @@ class GetNotificationsStatusUseCaseTest { ) ) fakeSession.pushersService().givenPushersLive(pushers) - every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false + every { fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns flowOf(false) every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true) // When @@ -109,7 +110,7 @@ class GetNotificationsStatusUseCaseTest { fun `given toggle via pusher is supported and no registered pusher when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { // Given fakeSession.pushersService().givenPushersLive(emptyList()) - every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false + every { fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns flowOf(false) every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true) // When @@ -120,7 +121,7 @@ class GetNotificationsStatusUseCaseTest { } @Test - fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on settings value`() = runTest { + fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on account data`() = runTest { // Given fakeSession .accountDataService() @@ -130,7 +131,7 @@ class GetNotificationsStatusUseCaseTest { isSilenced = false ).toContent(), ) - every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns true + every { fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns flowOf(true) every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false) // When @@ -138,5 +139,11 @@ class GetNotificationsStatusUseCaseTest { // Then result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED + verify { + fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) + } + verify(inverse = true) { + fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) + } } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt index c44fc4a4977..f1a0ae74520 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt @@ -16,6 +16,8 @@ package im.vector.app.test.fakes +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -25,6 +27,8 @@ import io.mockk.runs import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed = true) { @@ -32,6 +36,13 @@ class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed every { getUserAccountDataEvent(type) } returns content?.let { UserAccountDataEvent(type, it) } } + fun givenGetLiveUserAccountDataEventReturns(type: String, content: Content?): LiveData> { + return MutableLiveData(content?.let { UserAccountDataEvent(type, it) }.toOptional()) + .also { + every { getLiveUserAccountDataEvent(type) } returns it + } + } + fun givenUpdateUserAccountDataEventSucceeds() { coEvery { updateUserAccountData(any(), any()) } just runs } From c12af5a800f5bd436482ec118052ecabb76a3460 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 2 Dec 2022 14:14:51 +0100 Subject: [PATCH 103/197] Listening changes on notifications enabled preference to update the UI in settings --- ...SettingsNotificationPreferenceViewModel.kt | 27 ++++++++++++++ ...ingsNotificationPreferenceViewModelTest.kt | 35 +++++++++++++++++++ .../app/test/fakes/FakeVectorPreferences.kt | 7 ---- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt index 48e82b35e85..9530be599e3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt @@ -16,6 +16,8 @@ package im.vector.app.features.settings.notifications +import android.content.SharedPreferences +import androidx.annotation.VisibleForTesting import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -50,6 +52,31 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + @VisibleForTesting + val notificationsPreferenceListener: SharedPreferences.OnSharedPreferenceChangeListener = + SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) { + if (vectorPreferences.areNotificationEnabledForDevice()) { + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled) + } else { + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled) + } + } + } + + init { + observeNotificationsEnabledPreference() + } + + private fun observeNotificationsEnabledPreference() { + vectorPreferences.subscribeToChanges(notificationsPreferenceListener) + } + + override fun onCleared() { + vectorPreferences.unsubscribeToChanges(notificationsPreferenceListener) + super.onCleared() + } + override fun handle(action: VectorSettingsNotificationPreferenceViewAction) { when (action) { VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice -> handleDisableNotificationsForDevice() diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt index 270447c461f..ae36ee7600d 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt @@ -21,6 +21,7 @@ import im.vector.app.core.platform.VectorDummyViewState import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase import im.vector.app.core.pushers.RegisterUnifiedPushUseCase import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase +import im.vector.app.features.settings.VectorPreferences.Companion.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY import im.vector.app.test.fakes.FakePushersManager import im.vector.app.test.fakes.FakeVectorPreferences import im.vector.app.test.test @@ -60,6 +61,40 @@ class VectorSettingsNotificationPreferenceViewModelTest { toggleNotificationsForCurrentSessionUseCase = fakeToggleNotificationsForCurrentSessionUseCase, ) + @Test + fun `given view model init when notifications are enabled in preferences then view event is posted`() { + // Given + fakeVectorPreferences.givenAreNotificationsEnabledForDevice(true) + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled + val viewModel = createViewModel() + + // When + val viewModelTest = viewModel.test() + viewModel.notificationsPreferenceListener.onSharedPreferenceChanged(mockk(), SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + } + + @Test + fun `given view model init when notifications are disabled in preferences then view event is posted`() { + // Given + fakeVectorPreferences.givenAreNotificationsEnabledForDevice(false) + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled + val viewModel = createViewModel() + + // When + val viewModelTest = viewModel.test() + viewModel.notificationsPreferenceListener.onSharedPreferenceChanged(mockk(), SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + } + @Test fun `given DisableNotificationsForDevice action when handling action then disable use case is called`() { // Given diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index 3d7de662bd0..58bc1a18b87 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -16,7 +16,6 @@ package im.vector.app.test.fakes -import android.content.SharedPreferences.OnSharedPreferenceChangeListener import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences import io.mockk.every @@ -78,10 +77,4 @@ class FakeVectorPreferences { fun givenIsBackgroundSyncEnabled(isEnabled: Boolean) { every { instance.isBackgroundSyncEnabled() } returns isEnabled } - - fun givenChangeOnPreference(key: String) { - every { instance.subscribeToChanges(any()) } answers { - firstArg().onSharedPreferenceChanged(mockk(), key) - } - } } From b8023d66debbcff06df6f1c6bdd47a8fc41a3f0c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 5 Dec 2022 09:57:02 +0100 Subject: [PATCH 104/197] Fix formatting --- vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt b/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt index 970bb15a258..a5cba20561c 100644 --- a/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt +++ b/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt @@ -32,4 +32,3 @@ fun createPaparazziRule() = Paparazzi( theme = "Theme.Vector.Light", maxPercentDifference = 0.0, ) - From b4792c8a59e66715ee68f8ac650e712562b2b4a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 10:20:02 +0100 Subject: [PATCH 105/197] Bump leakcanary-android from 2.9.1 to 2.10 (#7570) --- vector-app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 68e20996adb..fa6aa5f0fd5 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -404,6 +404,6 @@ dependencies { androidTestImplementation libs.androidx.fragmentTesting androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22" debugImplementation libs.androidx.fragmentTesting - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' } From bbc756136cfa4b5d8dbc3baea00f88932b818b1f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 5 Dec 2022 10:26:07 +0100 Subject: [PATCH 106/197] Adding the rename and signout actions in the menu --- .../v2/VectorSettingsDevicesFragment.kt | 54 ++++++++++++------- .../res/menu/menu_current_session_header.xml | 10 ++++ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index d748600416d..64744f6eeb6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -101,6 +101,7 @@ class VectorSettingsDevicesFragment : initWaitingView() initCurrentSessionHeaderView() + initCurrentSessionListView() initOtherSessionsHeaderView() initOtherSessionsView() initSecurityRecommendationsView() @@ -153,6 +154,12 @@ class VectorSettingsDevicesFragment : } } + private fun initCurrentSessionListView() { + views.deviceListCurrentSession.viewVerifyButton.debouncedClicks { + viewModel.handle(DevicesAction.VerifyCurrentSession) + } + } + private fun initOtherSessionsHeaderView() { views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { @@ -351,31 +358,38 @@ class VectorSettingsDevicesFragment : private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?, hasOtherDevices: Boolean) { currentDeviceInfo?.let { - views.deviceListHeaderCurrentSession.isVisible = true - val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError) - val signoutOtherSessionsItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignoutOtherSessions) - signoutOtherSessionsItem.setTextColor(colorDestructive) - signoutOtherSessionsItem.isVisible = hasOtherDevices - views.deviceListCurrentSession.isVisible = true - val viewState = SessionInfoViewState( - isCurrentSession = true, - deviceFullInfo = it - ) - views.deviceListCurrentSession.render(viewState, dateFormatter, drawableProvider, colorProvider, stringProvider) - views.deviceListCurrentSession.debouncedClicks { - currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) } - } - views.deviceListCurrentSession.viewDetailsButton.debouncedClicks { - currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) } - } - views.deviceListCurrentSession.viewVerifyButton.debouncedClicks { - viewModel.handle(DevicesAction.VerifyCurrentSession) - } + renderCurrentSessionHeaderView(hasOtherDevices) + renderCurrentSessionListView(it) } ?: run { hideCurrentSessionView() } } + private fun renderCurrentSessionHeaderView(hasOtherDevices: Boolean) { + views.deviceListHeaderCurrentSession.isVisible = true + val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError) + val signoutSessionItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignout) + signoutSessionItem.setTextColor(colorDestructive) + val signoutOtherSessionsItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignoutOtherSessions) + signoutOtherSessionsItem.setTextColor(colorDestructive) + signoutOtherSessionsItem.isVisible = hasOtherDevices + } + + private fun renderCurrentSessionListView(currentDeviceInfo: DeviceFullInfo) { + views.deviceListCurrentSession.isVisible = true + val viewState = SessionInfoViewState( + isCurrentSession = true, + deviceFullInfo = currentDeviceInfo + ) + views.deviceListCurrentSession.render(viewState, dateFormatter, drawableProvider, colorProvider, stringProvider) + views.deviceListCurrentSession.debouncedClicks { + currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) } + } + views.deviceListCurrentSession.viewDetailsButton.debouncedClicks { + currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) } + } + } + private fun navigateToSessionOverview(deviceId: String) { viewNavigator.navigateToSessionOverview( context = requireActivity(), diff --git a/vector/src/main/res/menu/menu_current_session_header.xml b/vector/src/main/res/menu/menu_current_session_header.xml index 3b00423488b..993bee61782 100644 --- a/vector/src/main/res/menu/menu_current_session_header.xml +++ b/vector/src/main/res/menu/menu_current_session_header.xml @@ -4,6 +4,16 @@ xmlns:tools="http://schemas.android.com/tools" tools:ignore="AlwaysShowAction"> + + + + Date: Mon, 5 Dec 2022 09:42:20 +0000 Subject: [PATCH 107/197] Bump wysiwyg from 0.7.0.1 to 0.8.0 (#7666) Bumps [wysiwyg](https://github.com/matrix-org/matrix-wysiwyg) from 0.7.0.1 to 0.8.0. - [Release notes](https://github.com/matrix-org/matrix-wysiwyg/releases) - [Changelog](https://github.com/matrix-org/matrix-rich-text-editor/blob/main/CHANGELOG.md) - [Commits](https://github.com/matrix-org/matrix-wysiwyg/commits/0.8.0) --- updated-dependencies: - dependency-name: io.element.android:wysiwyg dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 27bca6434ff..fd630eba6d0 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -98,7 +98,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:0.7.0.1" + 'wysiwyg' : "io.element.android:wysiwyg:0.8.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", From 540758d66b0730d408a9737142ecaa01b7ccfd93 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 5 Dec 2022 10:43:56 +0100 Subject: [PATCH 108/197] Navigate to rename session screen from current session menu --- .../v2/VectorSettingsDevicesFragment.kt | 14 ++++++++ .../v2/VectorSettingsDevicesViewNavigator.kt | 5 +++ .../VectorSettingsDevicesViewNavigatorTest.kt | 35 +++++++++++++++---- .../OtherSessionsViewNavigatorTest.kt | 8 ++--- .../SessionOverviewViewNavigatorTest.kt | 8 ++--- .../im/vector/app/test/fakes/FakeContext.kt | 10 ++++-- 6 files changed, 61 insertions(+), 19 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 64744f6eeb6..c29655a0c7f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -145,6 +145,10 @@ class VectorSettingsDevicesFragment : private fun initCurrentSessionHeaderView() { views.deviceListHeaderCurrentSession.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { + R.id.currentSessionHeaderRename -> { + navigateToRenameCurrentSession() + true + } R.id.currentSessionHeaderSignoutOtherSessions -> { confirmMultiSignoutOtherSessions() true @@ -154,6 +158,16 @@ class VectorSettingsDevicesFragment : } } + private fun navigateToRenameCurrentSession() = withState(viewModel) { state -> + val currentDeviceId = state.currentSessionCrossSigningInfo.deviceId + if (currentDeviceId.isNotEmpty()) { + viewNavigator.navigateToRenameSession( + context = requireActivity(), + deviceId = currentDeviceId, + ) + } + } + private fun initCurrentSessionListView() { views.deviceListCurrentSession.viewVerifyButton.debouncedClicks { viewModel.handle(DevicesAction.VerifyCurrentSession) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt index 47e697822b2..d4b3345fead 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt @@ -20,6 +20,7 @@ import android.content.Context import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity +import im.vector.app.features.settings.devices.v2.rename.RenameSessionActivity import javax.inject.Inject class VectorSettingsDevicesViewNavigator @Inject constructor() { @@ -38,4 +39,8 @@ class VectorSettingsDevicesViewNavigator @Inject constructor() { OtherSessionsActivity.newIntent(context, titleResourceId, defaultFilter, excludeCurrentDevice) ) } + + fun navigateToRenameSession(context: Context, deviceId: String) { + context.startActivity(RenameSessionActivity.newIntent(context, deviceId)) + } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt index ec8019384ad..37823f7d530 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt @@ -20,6 +20,7 @@ import android.content.Intent import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity +import im.vector.app.features.settings.devices.v2.rename.RenameSessionActivity import im.vector.app.test.fakes.FakeContext import io.mockk.every import io.mockk.mockk @@ -43,6 +44,7 @@ class VectorSettingsDevicesViewNavigatorTest { fun setUp() { mockkObject(SessionOverviewActivity.Companion) mockkObject(OtherSessionsActivity.Companion) + mockkObject(RenameSessionActivity.Companion) } @After @@ -52,26 +54,41 @@ class VectorSettingsDevicesViewNavigatorTest { @Test fun `given a session id when navigating to overview then it starts the correct activity`() { + // Given val intent = givenIntentForSessionOverview(A_SESSION_ID) context.givenStartActivity(intent) + // When vectorSettingsDevicesViewNavigator.navigateToSessionOverview(context.instance, A_SESSION_ID) - verify { - context.instance.startActivity(intent) - } + // Then + context.verifyStartActivity(intent) } @Test fun `given an intent when navigating to other sessions list then it starts the correct activity`() { + // Given val intent = givenIntentForOtherSessions(A_TITLE_RESOURCE_ID, A_DEFAULT_FILTER, true) context.givenStartActivity(intent) + // When vectorSettingsDevicesViewNavigator.navigateToOtherSessions(context.instance, A_TITLE_RESOURCE_ID, A_DEFAULT_FILTER, true) - verify { - context.instance.startActivity(intent) - } + // Then + context.verifyStartActivity(intent) + } + + @Test + fun `given an intent when navigating to rename session screen then it starts the correct activity`() { + // Given + val intent = givenIntentForRenameSession(A_SESSION_ID) + context.givenStartActivity(intent) + + // When + vectorSettingsDevicesViewNavigator.navigateToRenameSession(context.instance, A_SESSION_ID) + + // Then + context.verifyStartActivity(intent) } private fun givenIntentForSessionOverview(sessionId: String): Intent { @@ -85,4 +102,10 @@ class VectorSettingsDevicesViewNavigatorTest { every { OtherSessionsActivity.newIntent(context.instance, titleResourceId, defaultFilter, excludeCurrentDevice) } returns intent return intent } + + private fun givenIntentForRenameSession(sessionId: String): Intent { + val intent = mockk() + every { RenameSessionActivity.newIntent(context.instance, sessionId) } returns intent + return intent + } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigatorTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigatorTest.kt index 3123572521d..23fc1cbdcef 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigatorTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigatorTest.kt @@ -23,7 +23,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject import io.mockk.unmockkAll -import io.mockk.verify import org.junit.After import org.junit.Before import org.junit.Test @@ -47,14 +46,15 @@ class OtherSessionsViewNavigatorTest { @Test fun `given a device id when navigating to overview then it starts the correct activity`() { + // Given val intent = givenIntentForDeviceOverview(A_DEVICE_ID) context.givenStartActivity(intent) + // When otherSessionsViewNavigator.navigateToSessionOverview(context.instance, A_DEVICE_ID) - verify { - context.instance.startActivity(intent) - } + // Then + context.verifyStartActivity(intent) } private fun givenIntentForDeviceOverview(deviceId: String): Intent { diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigatorTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigatorTest.kt index e309c05042d..99f30136974 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigatorTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigatorTest.kt @@ -60,9 +60,7 @@ class SessionOverviewViewNavigatorTest { sessionOverviewViewNavigator.goToSessionDetails(context.instance, A_SESSION_ID) // Then - verify { - context.instance.startActivity(intent) - } + context.verifyStartActivity(intent) } @Test @@ -75,9 +73,7 @@ class SessionOverviewViewNavigatorTest { sessionOverviewViewNavigator.goToRenameSession(context.instance, A_SESSION_ID) // Then - verify { - context.instance.startActivity(intent) - } + context.verifyStartActivity(intent) } @Test diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt index 9a94313fecf..22e191e29c9 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt @@ -24,9 +24,9 @@ import android.net.ConnectivityManager import android.net.Uri import android.os.ParcelFileDescriptor import io.mockk.every -import io.mockk.just +import io.mockk.justRun import io.mockk.mockk -import io.mockk.runs +import io.mockk.verify import java.io.OutputStream class FakeContext( @@ -73,7 +73,11 @@ class FakeContext( } fun givenStartActivity(intent: Intent) { - every { instance.startActivity(intent) } just runs + justRun { instance.startActivity(intent) } + } + + fun verifyStartActivity(intent: Intent) { + verify { instance.startActivity(intent) } } fun givenClipboardManager(): FakeClipboardManager { From 57554c5d3611642023669279d65d4c8c909b2b51 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 5 Dec 2022 14:10:56 +0100 Subject: [PATCH 109/197] Handling signout current session action --- .../settings/devices/v2/VectorSettingsDevicesFragment.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index c29655a0c7f..a21c7accb75 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -52,6 +52,7 @@ import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationVie import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase +import im.vector.app.features.workers.signout.SignOutUiWorker import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel @@ -149,6 +150,10 @@ class VectorSettingsDevicesFragment : navigateToRenameCurrentSession() true } + R.id.currentSessionHeaderSignout -> { + confirmSignoutCurrentSession() + true + } R.id.currentSessionHeaderSignoutOtherSessions -> { confirmMultiSignoutOtherSessions() true @@ -168,6 +173,10 @@ class VectorSettingsDevicesFragment : } } + private fun confirmSignoutCurrentSession() { + activity?.let { SignOutUiWorker(it).perform() } + } + private fun initCurrentSessionListView() { views.deviceListCurrentSession.viewVerifyButton.debouncedClicks { viewModel.handle(DevicesAction.VerifyCurrentSession) From a00508e08588568c8a85d14aa0f591ca31393b60 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 5 Dec 2022 14:12:00 +0100 Subject: [PATCH 110/197] Removing unused import --- .../devices/v2/VectorSettingsDevicesViewNavigatorTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt index 37823f7d530..24582c75d8b 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt @@ -26,7 +26,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject import io.mockk.unmockkAll -import io.mockk.verify import org.junit.After import org.junit.Before import org.junit.Test From 516103e51b2aa132f577166e2a004acaed57bbb1 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 5 Dec 2022 18:10:22 +0300 Subject: [PATCH 111/197] Fix usage of unknown shield in room summary. --- .../app/core/ui/views/ShieldImageView.kt | 25 ++++++++++++++++--- .../features/settings/devices/DeviceItem.kt | 4 +-- .../devices/v2/list/OtherSessionItem.kt | 2 +- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt index 6327daec86d..0570bbe4d75 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt @@ -38,6 +38,25 @@ class ShieldImageView @JvmOverloads constructor( } } + /** + * Renders device shield with the support of unknown shields instead of black shields which is used for rooms. + * @param roomEncryptionTrustLevel trust level that is usally calculated with [im.vector.app.features.settings.devices.TrustUtils.shieldForTrust] + * @param borderLess if true then the shield icon with border around is used + */ + fun renderDeviceShield(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, borderLess: Boolean = false) { + isVisible = roomEncryptionTrustLevel != null + + if (roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Default) { + contentDescription = context.getString(R.string.a11y_trust_level_default) + setImageResource( + if (borderLess) R.drawable.ic_shield_unknown_no_border + else R.drawable.ic_shield_unknown + ) + } else { + render(roomEncryptionTrustLevel, borderLess) + } + } + fun render(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, borderLess: Boolean = false) { isVisible = roomEncryptionTrustLevel != null @@ -45,8 +64,8 @@ class ShieldImageView @JvmOverloads constructor( RoomEncryptionTrustLevel.Default -> { contentDescription = context.getString(R.string.a11y_trust_level_default) setImageResource( - if (borderLess) R.drawable.ic_shield_unknown_no_border - else R.drawable.ic_shield_unknown + if (borderLess) R.drawable.ic_shield_black_no_border + else R.drawable.ic_shield_black ) } RoomEncryptionTrustLevel.Warning -> { @@ -137,7 +156,7 @@ class ShieldImageView @JvmOverloads constructor( @DrawableRes fun RoomEncryptionTrustLevel.toDrawableRes(): Int { return when (this) { - RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_unknown + RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_black RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning RoomEncryptionTrustLevel.Trusted -> R.drawable.ic_shield_trusted RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm -> R.drawable.ic_warning_badge diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt index 6486b8a3ca8..5924742c267 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt @@ -85,9 +85,9 @@ abstract class DeviceItem : VectorEpoxyModel(R.layout.item_de trusted ) - holder.trustIcon.render(shield) + holder.trustIcon.renderDeviceShield(shield) } else { - holder.trustIcon.render(null) + holder.trustIcon.renderDeviceShield(null) } val detailedModeLabels = listOf( diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt index 9d9cb15c288..68cae344cdb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt @@ -97,7 +97,7 @@ abstract class OtherSessionItem : VectorEpoxyModel(R.la } else { setDeviceTypeIconUseCase.execute(deviceType, holder.otherSessionDeviceTypeImageView, stringProvider) } - holder.otherSessionVerificationStatusImageView.render(roomEncryptionTrustLevel) + holder.otherSessionVerificationStatusImageView.renderDeviceShield(roomEncryptionTrustLevel) holder.otherSessionNameTextView.text = sessionName holder.otherSessionDescriptionTextView.text = sessionDescription sessionDescriptionColor?.let { From 32ded289fcf91b46c45df3c6cbfa92dbddb71d29 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 5 Dec 2022 18:18:09 +0300 Subject: [PATCH 112/197] Add changelog. --- changelog.d/7710.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7710.bugfix diff --git a/changelog.d/7710.bugfix b/changelog.d/7710.bugfix new file mode 100644 index 00000000000..9e75a03e1b9 --- /dev/null +++ b/changelog.d/7710.bugfix @@ -0,0 +1 @@ +Fix usage of unknown shield in room summary From febf01a2e634c51fc4cdf1c1ff64d26230731369 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 5 Dec 2022 16:36:16 +0100 Subject: [PATCH 113/197] Use the API `startForeground(int id, @NonNull Notification notification, @ForegroundServiceType int foregroundServiceType)` when available. Add missing android:foregroundServiceType in the manifest --- vector/src/main/AndroidManifest.xml | 6 ++- .../im/vector/app/core/extensions/Service.kt | 38 +++++++++++++++++++ .../app/core/services/CallAndroidService.kt | 13 ++++--- .../core/services/VectorSyncAndroidService.kt | 3 +- .../webrtc/ScreenCaptureAndroidService.kt | 3 +- .../tracking/LocationSharingAndroidService.kt | 3 +- .../features/start/StartAppAndroidService.kt | 3 +- 7 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/extensions/Service.kt diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index a26be23456d..9c8186b2d41 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -72,7 +72,9 @@ - + @@ -341,6 +344,7 @@ Int)? = null +) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground( + id, + notification, + provideForegroundServiceType?.invoke() ?: ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST + ) + } else { + startForeground(id, notification) + } +} diff --git a/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt b/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt index 85ea7f1a1b5..a4e3872e0f2 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt @@ -28,6 +28,7 @@ import androidx.media.session.MediaButtonReceiver import com.airbnb.mvrx.Mavericks import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.singletonEntryPoint +import im.vector.app.core.extensions.startForegroundCompat import im.vector.app.features.call.CallArgs import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.telecom.CallConnection @@ -181,7 +182,7 @@ class CallAndroidService : VectorAndroidService() { fromBg = fromBg ) if (knownCalls.isEmpty()) { - startForeground(callId.hashCode(), notification) + startForegroundCompat(callId.hashCode(), notification) } else { notificationManager.notify(callId.hashCode(), notification) } @@ -201,7 +202,7 @@ class CallAndroidService : VectorAndroidService() { } val notification = notificationUtils.buildCallEndedNotification(false) val notificationId = callId.hashCode() - startForeground(notificationId, notification) + startForegroundCompat(notificationId, notification) if (knownCalls.isEmpty()) { Timber.tag(loggerTag.value).v("No more call, stop the service") stopForegroundCompat() @@ -236,7 +237,7 @@ class CallAndroidService : VectorAndroidService() { title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId ) if (knownCalls.isEmpty()) { - startForeground(callId.hashCode(), notification) + startForegroundCompat(callId.hashCode(), notification) } else { notificationManager.notify(callId.hashCode(), notification) } @@ -260,7 +261,7 @@ class CallAndroidService : VectorAndroidService() { title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId ) if (knownCalls.isEmpty()) { - startForeground(callId.hashCode(), notification) + startForegroundCompat(callId.hashCode(), notification) } else { notificationManager.notify(callId.hashCode(), notification) } @@ -273,9 +274,9 @@ class CallAndroidService : VectorAndroidService() { callRingPlayerOutgoing?.stop() val notification = notificationUtils.buildCallEndedNotification(false) if (callId != null) { - startForeground(callId.hashCode(), notification) + startForegroundCompat(callId.hashCode(), notification) } else { - startForeground(DEFAULT_NOTIFICATION_ID, notification) + startForegroundCompat(DEFAULT_NOTIFICATION_ID, notification) } if (knownCalls.isEmpty()) { mediaSession?.isActive = false diff --git a/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt b/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt index 864f69a1366..f746c0749bb 100644 --- a/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt +++ b/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt @@ -32,6 +32,7 @@ import androidx.work.Worker import androidx.work.WorkerParameters import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.extensions.startForegroundCompat import im.vector.app.core.platform.PendingIntentCompat import im.vector.app.core.time.Clock import im.vector.app.core.time.DefaultClock @@ -98,7 +99,7 @@ class VectorSyncAndroidService : SyncAndroidService() { R.string.notification_listening_for_notifications } val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false) - startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification) + startForegroundCompat(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification) } override fun onRescheduleAsked( diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt index e7cebfb9c99..00b6bc40d25 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt @@ -20,6 +20,7 @@ import android.content.Intent import android.os.Binder import android.os.IBinder import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.startForegroundCompat import im.vector.app.core.services.VectorAndroidService import im.vector.app.core.time.Clock import im.vector.app.features.notifications.NotificationUtils @@ -41,7 +42,7 @@ class ScreenCaptureAndroidService : VectorAndroidService() { private fun showStickyNotification() { val notificationId = clock.epochMillis().toInt() val notification = notificationUtils.buildScreenSharingNotification() - startForeground(notificationId, notification) + startForegroundCompat(notificationId, notification) } override fun onBind(intent: Intent?): IBinder { diff --git a/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt b/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt index ccab23a83b6..d77a87f7567 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt @@ -22,6 +22,7 @@ import android.os.Parcelable import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.extensions.startForegroundCompat import im.vector.app.core.services.VectorAndroidService import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationTracker @@ -105,7 +106,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca if (foregroundModeStarted) { NotificationManagerCompat.from(this).notify(FOREGROUND_SERVICE_NOTIFICATION_ID, notification) } else { - startForeground(FOREGROUND_SERVICE_NOTIFICATION_ID, notification) + startForegroundCompat(FOREGROUND_SERVICE_NOTIFICATION_ID, notification) foregroundModeStarted = true } diff --git a/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt b/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt index e8e0eac8639..a14967a931e 100644 --- a/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt +++ b/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt @@ -20,6 +20,7 @@ import android.content.Intent import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.NamedGlobalScope +import im.vector.app.core.extensions.startForegroundCompat import im.vector.app.core.services.VectorAndroidService import im.vector.app.features.notifications.NotificationUtils import kotlinx.coroutines.CoroutineScope @@ -58,6 +59,6 @@ class StartAppAndroidService : VectorAndroidService() { private fun showStickyNotification() { val notificationId = Random.nextInt() val notification = notificationUtils.buildStartAppNotification() - startForeground(notificationId, notification) + startForegroundCompat(notificationId, notification) } } From 7b830d1c1a0679977ba5b2f2155f71aa86a4b04e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 5 Dec 2022 17:40:38 +0100 Subject: [PATCH 114/197] Renaming a use case --- ...anToggleNotificationsViaAccountDataUseCase.kt | 4 ++-- ...ficationSettingsAccountDataUpdatesUseCase.kt} | 2 +- ...ggleNotificationsViaAccountDataUseCaseTest.kt | 16 ++++++++-------- ...tionSettingsAccountDataUpdatesUseCaseTest.kt} | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) rename vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/{GetNotificationSettingsAccountDataAsFlowUseCase.kt => GetNotificationSettingsAccountDataUpdatesUseCase.kt} (94%) rename vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/{GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt => GetNotificationSettingsAccountDataUpdatesUseCaseTest.kt} (87%) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt index ac466852ebc..18ee9ad937a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt @@ -22,11 +22,11 @@ import org.matrix.android.sdk.api.session.Session import javax.inject.Inject class CanToggleNotificationsViaAccountDataUseCase @Inject constructor( - private val getNotificationSettingsAccountDataAsFlowUseCase: GetNotificationSettingsAccountDataAsFlowUseCase, + private val getNotificationSettingsAccountDataUpdatesUseCase: GetNotificationSettingsAccountDataUpdatesUseCase, ) { fun execute(session: Session, deviceId: String): Flow { - return getNotificationSettingsAccountDataAsFlowUseCase.execute(session, deviceId) + return getNotificationSettingsAccountDataUpdatesUseCase.execute(session, deviceId) .map { it?.isSilenced != null } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCase.kt similarity index 94% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCase.kt index ea4bd40f1f2..308aeec5f2a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCase.kt @@ -25,7 +25,7 @@ import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.toModel import javax.inject.Inject -class GetNotificationSettingsAccountDataAsFlowUseCase @Inject constructor() { +class GetNotificationSettingsAccountDataUpdatesUseCase @Inject constructor() { fun execute(session: Session, deviceId: String): Flow { return session diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt index a1dfed69029..b85acb1e696 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt @@ -29,10 +29,10 @@ import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent class CanToggleNotificationsViaAccountDataUseCaseTest { - private val fakeGetNotificationSettingsAccountDataAsFlowUseCase = mockk() + private val fakeGetNotificationSettingsAccountDataUpdatesUseCase = mockk() private val canToggleNotificationsViaAccountDataUseCase = CanToggleNotificationsViaAccountDataUseCase( - getNotificationSettingsAccountDataAsFlowUseCase = fakeGetNotificationSettingsAccountDataAsFlowUseCase, + getNotificationSettingsAccountDataUpdatesUseCase = fakeGetNotificationSettingsAccountDataUpdatesUseCase, ) @Test @@ -43,14 +43,14 @@ class CanToggleNotificationsViaAccountDataUseCaseTest { val localNotificationSettingsContent = LocalNotificationSettingsContent( isSilenced = true, ) - every { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(any(), any()) } returns flowOf(localNotificationSettingsContent) + every { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(any(), any()) } returns flowOf(localNotificationSettingsContent) // When val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull() // Then result shouldBe true - verify { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId) } + verify { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId) } } @Test @@ -61,14 +61,14 @@ class CanToggleNotificationsViaAccountDataUseCaseTest { val localNotificationSettingsContent = LocalNotificationSettingsContent( isSilenced = null, ) - every { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(any(), any()) } returns flowOf(localNotificationSettingsContent) + every { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(any(), any()) } returns flowOf(localNotificationSettingsContent) // When val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull() // Then result shouldBe false - verify { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId) } + verify { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId) } } @Test @@ -76,13 +76,13 @@ class CanToggleNotificationsViaAccountDataUseCaseTest { // Given val aSession = FakeSession() val aDeviceId = "aDeviceId" - every { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(any(), any()) } returns flowOf(null) + every { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(any(), any()) } returns flowOf(null) // When val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull() // Then result shouldBe false - verify { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId) } + verify { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId) } } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCaseTest.kt similarity index 87% rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCaseTest.kt index 6280d4c48b4..50940b9d346 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCaseTest.kt @@ -30,10 +30,10 @@ import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.toContent -class GetNotificationSettingsAccountDataAsFlowUseCaseTest { +class GetNotificationSettingsAccountDataUpdatesUseCaseTest { private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions() - private val getNotificationSettingsAccountDataAsFlowUseCase = GetNotificationSettingsAccountDataAsFlowUseCase() + private val getNotificationSettingsAccountDataUpdatesUseCase = GetNotificationSettingsAccountDataUpdatesUseCase() @Before fun setUp() { @@ -60,7 +60,7 @@ class GetNotificationSettingsAccountDataAsFlowUseCaseTest { .givenAsFlow() // When - val result = getNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId).firstOrNull() + val result = getNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId).firstOrNull() // Then result shouldBeEqualTo expectedContent @@ -80,7 +80,7 @@ class GetNotificationSettingsAccountDataAsFlowUseCaseTest { .givenAsFlow() // When - val result = getNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId).firstOrNull() + val result = getNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId).firstOrNull() // Then result shouldBeEqualTo null @@ -101,7 +101,7 @@ class GetNotificationSettingsAccountDataAsFlowUseCaseTest { .givenAsFlow() // When - val result = getNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId).firstOrNull() + val result = getNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId).firstOrNull() // Then result shouldBeEqualTo expectedContent From f2952f2deee61693de49e6b703b913ba0d5d8d6d Mon Sep 17 00:00:00 2001 From: valere Date: Mon, 5 Dec 2022 18:15:30 +0100 Subject: [PATCH 115/197] add to device tracing id --- changelog.d/7708.misc | 1 + .../internal/crypto/tasks/SendToDeviceTask.kt | 44 ++++- .../session/sync/handler/CryptoSyncHandler.kt | 5 +- .../crypto/DefaultSendToDeviceTaskTest.kt | 161 ++++++++++++++++++ 4 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 changelog.d/7708.misc create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt diff --git a/changelog.d/7708.misc b/changelog.d/7708.misc new file mode 100644 index 00000000000..62733303955 --- /dev/null +++ b/changelog.d/7708.misc @@ -0,0 +1 @@ +Add tracing Id for to device messages diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt index fc4d422360c..1e6ceeb1381 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt @@ -17,14 +17,21 @@ package org.matrix.android.sdk.internal.crypto.tasks 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.toContent import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber import java.util.UUID import javax.inject.Inject +const val TO_DEVICE_TRACING_ID_KEY = "org.matrix.msgid" + +fun Event.toDeviceTracingId(): String? = content?.get(TO_DEVICE_TRACING_ID_KEY) as? String + internal interface SendToDeviceTask : Task { data class Params( // the type of event to send @@ -42,15 +49,17 @@ internal class DefaultSendToDeviceTask @Inject constructor( ) : SendToDeviceTask { override suspend fun execute(params: SendToDeviceTask.Params) { - val sendToDeviceBody = SendToDeviceBody( - messages = params.contentMap.map - ) - // If params.transactionId is not provided, we create a unique txnId. // It's important to do that outside the requestBlock parameter of executeRequest() // to use the same value if the request is retried val txnId = params.transactionId ?: createUniqueTxnId() + // add id tracing to debug + val decorated = decorateWithToDeviceTracingIds(params) + val sendToDeviceBody = SendToDeviceBody( + messages = decorated.first + ) + return executeRequest( globalErrorReceiver, canRetry = true, @@ -61,8 +70,35 @@ internal class DefaultSendToDeviceTask @Inject constructor( transactionId = txnId, body = sendToDeviceBody ) + Timber.i("Sent to device type=${params.eventType} txnid=$txnId [${decorated.second.joinToString(",")}]") } } + + /** + * To make it easier to track down where to-device messages are getting lost, + * add a custom property to each one, and that will be logged after sent and on reception. Synapse will also log + * this property. + * @return A pair, first is the decorated content, and second info to log out after sending + */ + private fun decorateWithToDeviceTracingIds(params: SendToDeviceTask.Params): Pair>, List> { + val tracingInfo = mutableListOf() + val decoratedContent = params.contentMap.map.map { userToDeviceMap -> + val userId = userToDeviceMap.key + userId to userToDeviceMap.value.map { + val deviceId = it.key + deviceId to it.value.toContent().toMutableMap().apply { + put( + TO_DEVICE_TRACING_ID_KEY, + UUID.randomUUID().toString().also { + tracingInfo.add("$userId/$deviceId (msgid $it)") + } + ) + } + }.toMap() + }.toMap() + + return decoratedContent to tracingInfo + } } internal fun createUniqueTxnId() = UUID.randomUUID().toString() 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 b2fe12ebc3f..291e785aa5c 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 @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.crypto.tasks.toDeviceTracingId import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService import org.matrix.android.sdk.internal.session.sync.ProgressReporter import timber.log.Timber @@ -48,12 +49,14 @@ internal class CryptoSyncHandler @Inject constructor( ?.forEachIndexed { index, event -> progressReporter?.reportProgress(index * 100F / total) // Decrypt event if necessary - Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}") + Timber.tag(loggerTag.value).d("To device event tracingId:${event.toDeviceTracingId()}") decryptToDeviceEvent(event, null) + if (event.getClearType() == EventType.MESSAGE && event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") { Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") } else { + Timber.tag(loggerTag.value).d("received to-device ${event.getClearType()} from:${event.senderId} id:${event.toDeviceTracingId()}") verificationService.onToDeviceEvent(event) cryptoService.onToDeviceEvent(event) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt new file mode 100644 index 00000000000..72d166c1f40 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt @@ -0,0 +1,161 @@ +/* + * 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 org.matrix.android.sdk.internal.crypto + +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.internal.assertEquals +import org.junit.Assert +import org.junit.Test +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams +import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse +import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody +import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse +import org.matrix.android.sdk.internal.crypto.model.rest.UpdateDeviceInfoBody +import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody +import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver + +class DefaultSendToDeviceTaskTest { + + val users = listOf( + "@alice:example.com" to listOf("D0", "D1"), + "bob@example.com" to listOf("D2", "D3") + ) + + val fakeEncryptedContent = mapOf( + "algorithm" to "m.olm.v1.curve25519-aes-sha2", + "sender_key" to "gMObR+/4dqL5T4DisRRRYBJpn+OjzFnkyCFOktP6Eyw", + "ciphertext" to mapOf( + "tdwXf7006FDgzmufMCVI4rDdVPO51ecRTTT6HkRxUwE" to mapOf( + "type" to 0, + "body" to "AwogCA1ULEc0abGIFxMDIC9iv7ul3jqJSnapTHQ+8JJx" + ) + ) + ) + + @Test + fun `tracing id should be added to all to_device contents`() { + val fakeCryptoAPi = FakeCryptoApi() + + val sendToDeviceTask = DefaultSendToDeviceTask( + cryptoApi = fakeCryptoAPi, + globalErrorReceiver = mockk(relaxed = true) + ) + + val contentMap = MXUsersDevicesMap() + + users.forEach { + val userId = it.first + it.second.forEach { + contentMap.setObject(userId, it, fakeEncryptedContent) + } + } + + val params = SendToDeviceTask.Params( + eventType = EventType.ENCRYPTED, + contentMap = contentMap + ) + + runBlocking { + sendToDeviceTask.execute(params) + } + + val generatedIds = mutableListOf() + users.forEach { + val userId = it.first + it.second.forEach { + val modifiedContent = fakeCryptoAPi.body!!.messages!![userId]!![it] as Map + Assert.assertNotNull("Tracing id should have been added", modifiedContent["org.matrix.msgid"]) + generatedIds.add(modifiedContent["org.matrix.msgid"] as String) + + assertEquals( + "The rest of the content should be the same", + fakeEncryptedContent.keys, + modifiedContent.toMutableMap().apply { remove("org.matrix.msgid") }.keys + ) + } + } + + assertEquals("Id should be unique per content", generatedIds.size, generatedIds.toSet().size) + println("modified content ${fakeCryptoAPi.body}") + } + + internal class FakeCryptoApi : CryptoApi { + override suspend fun getDevices(): DevicesListResponse { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun getDeviceInfo(deviceId: String): DeviceInfo { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun uploadKeys(body: KeysUploadBody): KeysUploadResponse { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun downloadKeysForUsers(params: KeysQueryBody): KeysQueryResponse { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun uploadSigningKeys(params: UploadSigningKeysBody): KeysQueryResponse { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun uploadSignatures(params: Map?): SignatureUploadResponse { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun claimOneTimeKeysForUsersDevices(body: KeysClaimBody): KeysClaimResponse { + throw java.lang.AssertionError("Should not be called") + } + + var body: SendToDeviceBody? = null + override suspend fun sendToDevice(eventType: String, transactionId: String, body: SendToDeviceBody) { + this.body = body + } + + override suspend fun deleteDevice(deviceId: String, params: DeleteDeviceParams) { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun deleteDevices(params: DeleteDevicesParams) { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun updateDeviceInfo(deviceId: String, params: UpdateDeviceInfoBody) { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun getKeyChanges(oldToken: String, newToken: String): KeyChangesResponse { + throw java.lang.AssertionError("Should not be called") + } + } +} From 2ed212aa11a36aed1df67722df77f8736f96ad4f Mon Sep 17 00:00:00 2001 From: valere Date: Mon, 5 Dec 2022 18:30:38 +0100 Subject: [PATCH 116/197] Fix copyright --- .../android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt index 72d166c1f40..c813991d3f3 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. From 139eb1708c3b5e7f9fb15d87c6fb2dd3346f8639 Mon Sep 17 00:00:00 2001 From: valere Date: Tue, 6 Dec 2022 08:17:31 +0100 Subject: [PATCH 117/197] fix uncheck cast warning --- .../crypto/DefaultSendToDeviceTaskTest.kt | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt index c813991d3f3..b8e870bd06a 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt @@ -41,16 +41,15 @@ import org.matrix.android.sdk.internal.crypto.model.rest.UpdateDeviceInfoBody import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.network.GlobalErrorReceiver class DefaultSendToDeviceTaskTest { - val users = listOf( + private val users = listOf( "@alice:example.com" to listOf("D0", "D1"), "bob@example.com" to listOf("D2", "D3") ) - val fakeEncryptedContent = mapOf( + private val fakeEncryptedContent = mapOf( "algorithm" to "m.olm.v1.curve25519-aes-sha2", "sender_key" to "gMObR+/4dqL5T4DisRRRYBJpn+OjzFnkyCFOktP6Eyw", "ciphertext" to mapOf( @@ -67,14 +66,14 @@ class DefaultSendToDeviceTaskTest { val sendToDeviceTask = DefaultSendToDeviceTask( cryptoApi = fakeCryptoAPi, - globalErrorReceiver = mockk(relaxed = true) + globalErrorReceiver = mockk(relaxed = true) ) val contentMap = MXUsersDevicesMap() - users.forEach { - val userId = it.first - it.second.forEach { + users.forEach { pairOfUserDevices -> + val userId = pairOfUserDevices.first + pairOfUserDevices.second.forEach { contentMap.setObject(userId, it, fakeEncryptedContent) } } @@ -89,10 +88,10 @@ class DefaultSendToDeviceTaskTest { } val generatedIds = mutableListOf() - users.forEach { - val userId = it.first - it.second.forEach { - val modifiedContent = fakeCryptoAPi.body!!.messages!![userId]!![it] as Map + users.forEach { pairOfUserDevices -> + val userId = pairOfUserDevices.first + pairOfUserDevices.second.forEach { + val modifiedContent = fakeCryptoAPi.body!!.messages!![userId]!![it] as Map<*, *> Assert.assertNotNull("Tracing id should have been added", modifiedContent["org.matrix.msgid"]) generatedIds.add(modifiedContent["org.matrix.msgid"] as String) From 4cd4cf1c51930954e22398f21dcf0312e14e0c05 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 6 Dec 2022 14:06:14 +0300 Subject: [PATCH 118/197] Code review fix. --- .../app/features/settings/devices/v2/list/SessionInfoView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt index c6044d04a49..7727cee4fa3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt @@ -90,7 +90,7 @@ class SessionInfoView @JvmOverloads constructor( hasLearnMoreLink: Boolean, isVerifyButtonVisible: Boolean, ) { - views.sessionInfoVerificationStatusImageView.render(encryptionTrustLevel) + views.sessionInfoVerificationStatusImageView.renderDeviceShield(encryptionTrustLevel) when { encryptionTrustLevel == RoomEncryptionTrustLevel.Trusted -> renderCrossSigningVerified(isCurrentSession) encryptionTrustLevel == RoomEncryptionTrustLevel.Default && !isCurrentSession -> renderCrossSigningUnknown() From a65e13970d9ca9a92feb913ec0e5ab71ddaac3d2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 6 Dec 2022 11:52:13 +0100 Subject: [PATCH 119/197] `appdistribution` is only for nightly builds, not necessary for gplay (prod) builds. --- vector-app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector-app/build.gradle b/vector-app/build.gradle index fa6aa5f0fd5..6ebb08600b4 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -374,7 +374,7 @@ dependencies { // API-only library gplayImplementation libs.google.appdistributionApi // Full SDK implementation - gplayImplementation libs.google.appdistribution + nightlyImplementation libs.google.appdistribution // OSS License, gplay flavor only gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' From 0d12dbbe7e8031aba0ca64e4da514c9e79636e01 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 6 Dec 2022 12:21:07 +0100 Subject: [PATCH 120/197] Disable the Nightly popup, user registration (with `updateIfNewReleaseAvailable()`) to get upgrade does not work. Add a nightly build section in the preferences to manually try to upgrade. --- .../src/main/res/values/strings.xml | 3 +++ .../app/nightly/FirebaseNightlyProxy.kt | 21 +++++++++++++++---- .../vector/app/features/home/HomeActivity.kt | 4 +++- .../vector/app/features/home/NightlyProxy.kt | 15 ++++++++++++- .../VectorSettingsAdvancedSettingsFragment.kt | 18 ++++++++++++++++ .../xml/vector_settings_advanced_settings.xml | 12 +++++++++++ 6 files changed, 67 insertions(+), 6 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 609cdac2338..7f11e63469f 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -2487,6 +2487,9 @@ Key Requests Export Audit + Nightly build + Get the latest build (note: you may have trouble to sign in) + Unlock encrypted messages history Refresh diff --git a/vector-app/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt b/vector-app/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt index 94a36036b6e..71ffda7c36a 100644 --- a/vector-app/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt +++ b/vector-app/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt @@ -34,8 +34,11 @@ class FirebaseNightlyProxy @Inject constructor( private val buildMeta: BuildMeta, ) : NightlyProxy { - override fun onHomeResumed() { - if (!canDisplayPopup()) return + override fun isNightlyBuild(): Boolean { + return buildMeta.applicationId in nightlyPackages + } + + override fun updateApplication() { val firebaseAppDistribution = FirebaseAppDistribution.getInstance() firebaseAppDistribution.updateIfNewReleaseAvailable() .addOnProgressListener { up -> @@ -46,6 +49,7 @@ class FirebaseNightlyProxy @Inject constructor( when (e.errorCode) { FirebaseAppDistributionException.Status.NOT_IMPLEMENTED -> { // SDK did nothing. This is expected when building for Play. + Timber.d("FirebaseAppDistribution NOT_IMPLEMENTED error") } else -> { // Handle other errors. @@ -56,10 +60,14 @@ class FirebaseNightlyProxy @Inject constructor( Timber.e(e, "FirebaseAppDistribution - other error") } } + .addOnSuccessListener { + Timber.d("FirebaseAppDistribution Success!") + } } - private fun canDisplayPopup(): Boolean { - if (buildMeta.applicationId != "im.vector.app.nightly") return false + override fun canDisplayPopup(): Boolean { + if (!POPUP_IS_ENABLED) return false + if (!isNightlyBuild()) return false val today = clock.epochMillis() / A_DAY_IN_MILLIS val lastDisplayPopupDay = sharedPreferences.getLong(SHARED_PREF_KEY, 0) return (today > lastDisplayPopupDay) @@ -73,7 +81,12 @@ class FirebaseNightlyProxy @Inject constructor( } companion object { + private const val POPUP_IS_ENABLED = false private const val A_DAY_IN_MILLIS = 8_600_000L private const val SHARED_PREF_KEY = "LAST_NIGHTLY_POPUP_DAY" + + private val nightlyPackages = listOf( + "im.vector.app.nightly" + ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 8c6daae95a1..2a3d8d094c4 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -580,7 +580,9 @@ class HomeActivity : serverBackupStatusViewModel.refreshRemoteStateIfNeeded() // Check nightly - nightlyProxy.onHomeResumed() + if (nightlyProxy.canDisplayPopup()) { + nightlyProxy.updateApplication() + } checkNewAppLayoutFlagChange() } diff --git a/vector/src/main/java/im/vector/app/features/home/NightlyProxy.kt b/vector/src/main/java/im/vector/app/features/home/NightlyProxy.kt index b25add2ac93..42b93bf1a55 100644 --- a/vector/src/main/java/im/vector/app/features/home/NightlyProxy.kt +++ b/vector/src/main/java/im/vector/app/features/home/NightlyProxy.kt @@ -17,5 +17,18 @@ package im.vector.app.features.home interface NightlyProxy { - fun onHomeResumed() + /** + * Return true if this is a nightly build (checking the package of the app), and only once a day. + */ + fun canDisplayPopup(): Boolean + + /** + * Return true if this is a nightly build (checking the package of the app). + */ + fun isNightlyBuild(): Boolean + + /** + * Try to update the application, if update is available. Will also take care of the user sign in. + */ + fun updateApplication() } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt index 9c08d446f4e..b6fa997f41c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt @@ -22,10 +22,13 @@ import androidx.preference.SeekBarPreference import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreferenceCategory import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.features.analytics.plan.MobileScreen +import im.vector.app.features.home.NightlyProxy import im.vector.app.features.rageshake.RageShake +import javax.inject.Inject @AndroidEntryPoint class VectorSettingsAdvancedSettingsFragment : @@ -34,6 +37,8 @@ class VectorSettingsAdvancedSettingsFragment : override var titleRes = R.string.settings_advanced_settings override val preferenceXmlRes = R.xml.vector_settings_advanced_settings + @Inject lateinit var nightlyProxy: NightlyProxy + private var rageshake: RageShake? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -57,6 +62,11 @@ class VectorSettingsAdvancedSettingsFragment : } override fun bindPref() { + setupRageShakeSection() + setupNightlySection() + } + + private fun setupRageShakeSection() { val isRageShakeAvailable = RageShake.isAvailable(requireContext()) if (isRageShakeAvailable) { @@ -86,4 +96,12 @@ class VectorSettingsAdvancedSettingsFragment : findPreference("SETTINGS_RAGE_SHAKE_CATEGORY_KEY")!!.isVisible = false } } + + private fun setupNightlySection() { + findPreference("SETTINGS_NIGHTLY_BUILD_PREFERENCE_KEY")?.isVisible = nightlyProxy.isNightlyBuild() + findPreference("SETTINGS_NIGHTLY_BUILD_UPDATE_PREFERENCE_KEY")?.setOnPreferenceClickListener { + nightlyProxy.updateApplication() + true + } + } } diff --git a/vector/src/main/res/xml/vector_settings_advanced_settings.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml index 29d80515838..9260b33162f 100644 --- a/vector/src/main/res/xml/vector_settings_advanced_settings.xml +++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml @@ -95,4 +95,16 @@ + + + + + + From ae93c075973b61d5a03b9842fa4bd1e0698bfb6f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 6 Dec 2022 15:01:47 +0100 Subject: [PATCH 121/197] Do not propagate failure if saving the filter server side fails. This will be retried later. --- .../session/filter/GetCurrentFilterTask.kt | 2 +- .../internal/session/filter/SaveFilterTask.kt | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt index e88f286e27b..76805c5c514 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt @@ -42,7 +42,7 @@ internal class DefaultGetCurrentFilterTask @Inject constructor( return when (storedFilterBody) { currentFilterBody -> storedFilterId ?: storedFilterBody - else -> saveFilter(currentFilter) + else -> saveFilter(currentFilter) ?: currentFilterBody } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt index 82d5ff4d2f7..0223cd3ee74 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.filter +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest @@ -24,8 +25,9 @@ import javax.inject.Inject /** * Save a filter, in db and if any changes, upload to the server. + * Return the filterId if uploading to the server is successful, else return null. */ -internal interface SaveFilterTask : Task { +internal interface SaveFilterTask : Task { data class Params( val filter: Filter @@ -39,18 +41,20 @@ internal class DefaultSaveFilterTask @Inject constructor( private val globalErrorReceiver: GlobalErrorReceiver, ) : SaveFilterTask { - override suspend fun execute(params: SaveFilterTask.Params): String { + override suspend fun execute(params: SaveFilterTask.Params): String? { val filter = params.filter - val filterResponse = executeRequest(globalErrorReceiver) { - // TODO auto retry - filterAPI.uploadFilter(userId, filter) + val filterResponse = tryOrNull { + executeRequest(globalErrorReceiver) { + filterAPI.uploadFilter(userId, filter) + } } + val filterId = filterResponse?.filterId filterRepository.storeSyncFilter( filter = filter, - filterId = filterResponse.filterId, + filterId = filterId.orEmpty(), roomEventFilter = FilterFactory.createDefaultRoomFilter() ) - return filterResponse.filterId + return filterId } } From 8646cc441d2c4c2d7e36c95f6dd3de71f5bd3dc3 Mon Sep 17 00:00:00 2001 From: valere Date: Tue, 6 Dec 2022 15:30:06 +0100 Subject: [PATCH 122/197] do not add tracing ids to verification events --- .../sdk/api/session/events/model/EventType.kt | 3 + .../internal/crypto/tasks/SendToDeviceTask.kt | 12 ++- .../crypto/DefaultSendToDeviceTaskTest.kt | 97 ++++++++++++++++++- 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index e5c14afa905..013b452ced2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.session.events.model +import org.matrix.android.sdk.api.session.room.model.message.MessageType.MSGTYPE_VERIFICATION_REQUEST + /** * Constants defining known event types from Matrix specifications. */ @@ -126,6 +128,7 @@ object EventType { fun isVerificationEvent(type: String): Boolean { return when (type) { + MSGTYPE_VERIFICATION_REQUEST, KEY_VERIFICATION_START, KEY_VERIFICATION_ACCEPT, KEY_VERIFICATION_KEY, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt index 1e6ceeb1381..a7e93202ef0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.tasks 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.toContent import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody @@ -39,7 +40,9 @@ internal interface SendToDeviceTask : Task { // the content to send. Map from user_id to device_id to content dictionary. val contentMap: MXUsersDevicesMap, // the transactionId. If not provided, a transactionId will be created by the task - val transactionId: String? = null + val transactionId: String? = null, + // add tracing id, notice that to device events that do signature on content might be broken by it + val addTracingIds: Boolean = !EventType.isVerificationEvent(eventType), ) } @@ -55,7 +58,12 @@ internal class DefaultSendToDeviceTask @Inject constructor( val txnId = params.transactionId ?: createUniqueTxnId() // add id tracing to debug - val decorated = decorateWithToDeviceTracingIds(params) + val decorated = if (params.addTracingIds) { + decorateWithToDeviceTracingIds(params) + } else { + params.contentMap.map to emptyList() + } + val sendToDeviceBody = SendToDeviceBody( messages = decorated.first ) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt index b8e870bd06a..df6fc5f165f 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams @@ -60,8 +61,28 @@ class DefaultSendToDeviceTaskTest { ) ) + private val fakeStartVerificationContent = mapOf( + "method" to "m.sas.v1", + "from_device" to "MNQHVEISFQ", + "key_agreement_protocols" to listOf( + "curve25519-hkdf-sha256", + "curve25519" + ), + "hashes" to listOf("sha256"), + "message_authentication_codes" to listOf( + "org.matrix.msc3783.hkdf-hmac-sha256", + "hkdf-hmac-sha256", + "hmac-sha256" + ), + "short_authentication_string" to listOf( + "decimal", + "emoji" + ), + "transaction_id" to "4wNOpkHGwGZPXjkZToooCDWfb8hsf7vW" + ) + @Test - fun `tracing id should be added to all to_device contents`() { + fun `tracing id should be added to to_device contents`() { val fakeCryptoAPi = FakeCryptoApi() val sendToDeviceTask = DefaultSendToDeviceTask( @@ -107,6 +128,80 @@ class DefaultSendToDeviceTaskTest { println("modified content ${fakeCryptoAPi.body}") } + @Test + fun `tracing id should not be added to verification start to_device contents`() { + val fakeCryptoAPi = FakeCryptoApi() + + val sendToDeviceTask = DefaultSendToDeviceTask( + cryptoApi = fakeCryptoAPi, + globalErrorReceiver = mockk(relaxed = true) + ) + val contentMap = MXUsersDevicesMap() + contentMap.setObject("@alice:example.com", "MNQHVEISFQ", fakeStartVerificationContent) + + val params = SendToDeviceTask.Params( + eventType = EventType.KEY_VERIFICATION_START, + contentMap = contentMap + ) + + runBlocking { + sendToDeviceTask.execute(params) + } + + val modifiedContent = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *> + Assert.assertNull("Tracing id should not have been added", modifiedContent["org.matrix.msgid"]) + + // try to force + runBlocking { + sendToDeviceTask.execute( + SendToDeviceTask.Params( + eventType = EventType.KEY_VERIFICATION_START, + contentMap = contentMap, + addTracingIds = true + ) + ) + } + + val modifiedContentForced = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *> + Assert.assertNotNull("Tracing id should have been added", modifiedContentForced["org.matrix.msgid"]) + } + + @Test + fun `tracing id should not be added to all verification to_device contents`() { + val fakeCryptoAPi = FakeCryptoApi() + + val sendToDeviceTask = DefaultSendToDeviceTask( + cryptoApi = fakeCryptoAPi, + globalErrorReceiver = mockk(relaxed = true) + ) + val contentMap = MXUsersDevicesMap() + contentMap.setObject("@alice:example.com", "MNQHVEISFQ", emptyMap()) + + val verificationEvents = listOf( + MessageType.MSGTYPE_VERIFICATION_REQUEST, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_READY + ) + + for (type in verificationEvents) { + val params = SendToDeviceTask.Params( + eventType = type, + contentMap = contentMap + ) + runBlocking { + sendToDeviceTask.execute(params) + } + + val modifiedContent = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *> + Assert.assertNull("Tracing id should not have been added", modifiedContent["org.matrix.msgid"]) + } + } + internal class FakeCryptoApi : CryptoApi { override suspend fun getDevices(): DevicesListResponse { throw java.lang.AssertionError("Should not be called") From 63d2886415820d979c1e6592dd582123b1bb09ec Mon Sep 17 00:00:00 2001 From: valere Date: Tue, 6 Dec 2022 16:07:24 +0100 Subject: [PATCH 123/197] use msgid in logs for consistency --- .../sdk/internal/session/sync/handler/CryptoSyncHandler.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 291e785aa5c..551db52dbd7 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 @@ -49,14 +49,14 @@ internal class CryptoSyncHandler @Inject constructor( ?.forEachIndexed { index, event -> progressReporter?.reportProgress(index * 100F / total) // Decrypt event if necessary - Timber.tag(loggerTag.value).d("To device event tracingId:${event.toDeviceTracingId()}") + Timber.tag(loggerTag.value).d("To device event msgid:${event.toDeviceTracingId()}") decryptToDeviceEvent(event, null) if (event.getClearType() == EventType.MESSAGE && event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") { Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") } else { - Timber.tag(loggerTag.value).d("received to-device ${event.getClearType()} from:${event.senderId} id:${event.toDeviceTracingId()}") + Timber.tag(loggerTag.value).d("received to-device ${event.getClearType()} from:${event.senderId} msgid:${event.toDeviceTracingId()}") verificationService.onToDeviceEvent(event) cryptoService.onToDeviceEvent(event) } From a6752a0cf18fa974dd7ec5d7ba90954f21f1f17c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Dec 2022 16:10:25 +0000 Subject: [PATCH 124/197] Bump com.google.devtools.ksp from 1.7.21-1.0.8 to 1.7.22-1.0.8 Bumps [com.google.devtools.ksp](https://github.com/google/ksp) from 1.7.21-1.0.8 to 1.7.22-1.0.8. - [Release notes](https://github.com/google/ksp/releases) - [Commits](https://github.com/google/ksp/compare/1.7.21-1.0.8...1.7.22-1.0.8) --- updated-dependencies: - dependency-name: com.google.devtools.ksp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 58084ab64d1..2abb2a9072c 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,7 @@ plugins { // Detekt id "io.gitlab.arturbosch.detekt" version "1.22.0" // Ksp - id "com.google.devtools.ksp" version "1.7.21-1.0.8" + id "com.google.devtools.ksp" version "1.7.22-1.0.8" // Dependency Analysis id 'com.autonomousapps.dependency-analysis' version "1.17.0" From 988afa4ebe5354849972c5a8faa2a4a4a331c846 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 6 Dec 2022 18:21:07 +0100 Subject: [PATCH 125/197] Fix FDroid build --- vector-app/src/fdroid/java/im/vector/app/di/FlavorModule.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vector-app/src/fdroid/java/im/vector/app/di/FlavorModule.kt b/vector-app/src/fdroid/java/im/vector/app/di/FlavorModule.kt index 63b4c2a3cda..e7d9598b654 100644 --- a/vector-app/src/fdroid/java/im/vector/app/di/FlavorModule.kt +++ b/vector-app/src/fdroid/java/im/vector/app/di/FlavorModule.kt @@ -46,9 +46,9 @@ abstract class FlavorModule { @Provides fun provideNightlyProxy() = object : NightlyProxy { - override fun onHomeResumed() { - // no op - } + override fun canDisplayPopup() = false + override fun isNightlyBuild() = false + override fun updateApplication() = Unit } @Provides From 9bbecbeed33eba37af120ea004cd7c49fd539f21 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Dec 2022 23:02:30 +0000 Subject: [PATCH 126/197] Bump wysiwyg from 0.8.0 to 0.9.0 Bumps [wysiwyg](https://github.com/matrix-org/matrix-wysiwyg) from 0.8.0 to 0.9.0. - [Release notes](https://github.com/matrix-org/matrix-wysiwyg/releases) - [Changelog](https://github.com/matrix-org/matrix-rich-text-editor/blob/main/CHANGELOG.md) - [Commits](https://github.com/matrix-org/matrix-wysiwyg/compare/0.8.0...0.9.0) --- updated-dependencies: - dependency-name: io.element.android:wysiwyg dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index fd630eba6d0..b408ee01eb7 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -98,7 +98,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:0.8.0" + 'wysiwyg' : "io.element.android:wysiwyg:0.9.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", From 041fcef1db267de5f8dfcf954fb4f043d38238c0 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 7 Dec 2022 10:30:47 +0100 Subject: [PATCH 127/197] Adding changelog entry --- changelog.d/7733.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7733.bugfix diff --git a/changelog.d/7733.bugfix b/changelog.d/7733.bugfix new file mode 100644 index 00000000000..9de3759f1ac --- /dev/null +++ b/changelog.d/7733.bugfix @@ -0,0 +1 @@ +[Session manager] Sessions without encryption support should not prompt to verify From 11dded71ec0c642039214c6deec0d73f8c1ca039 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Dec 2022 13:54:20 +0100 Subject: [PATCH 128/197] Changelog --- changelog.d/7725.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7725.bugfix diff --git a/changelog.d/7725.bugfix b/changelog.d/7725.bugfix new file mode 100644 index 00000000000..b701451505f --- /dev/null +++ b/changelog.d/7725.bugfix @@ -0,0 +1 @@ +Fix crash when the network is not available. From c9c5483d227e80d3761596fecba6502ea000321c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Dec 2022 14:09:59 +0100 Subject: [PATCH 129/197] Changelog --- changelog.d/7723.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7723.misc diff --git a/changelog.d/7723.misc b/changelog.d/7723.misc new file mode 100644 index 00000000000..36869d1efbb --- /dev/null +++ b/changelog.d/7723.misc @@ -0,0 +1 @@ +Disable nightly popup and add an entry point in the advanced settings instead. From f014866d063a6c7d1df769c4c74cbd6aa7d916c9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 7 Dec 2022 14:34:45 +0100 Subject: [PATCH 130/197] Handling the case where device has no CryptoDeviceInfo --- .../src/main/res/values/strings.xml | 2 + .../app/core/ui/views/ShieldImageView.kt | 28 ++++++++------ .../settings/devices/DevicesViewModel.kt | 2 +- .../settings/devices/v2/DeviceFullInfo.kt | 2 +- .../devices/v2/list/SessionInfoView.kt | 11 +++++- .../v2/overview/SessionOverviewFragment.kt | 37 +++++++++++-------- ...GetEncryptionTrustLevelForDeviceUseCase.kt | 10 +++-- ...ncryptionTrustLevelForDeviceUseCaseTest.kt | 15 ++++++++ 8 files changed, 75 insertions(+), 32 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 609cdac2338..b10d11d0486 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3305,6 +3305,7 @@ Verify your current session for enhanced secure messaging. Verify or sign out from this session for best security and reliability. Verify your current session to reveal this session\'s verification status. + This session doesn\'t support encryption and thus can\'t be verified. Verify Session View Details View All (%1$d) @@ -3397,6 +3398,7 @@ Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you. Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session. + This session doesn\'t support encryption, so it can\'t be verified.\n\nYou won\'t be able to participate in rooms where encryption is enabled when using this session.\n\nFor best security and privacy, it is recommended to use Matrix clients that support encryption. Renaming sessions Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here. Enable new session manager diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt index 0570bbe4d75..34714d97d0f 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt @@ -40,20 +40,26 @@ class ShieldImageView @JvmOverloads constructor( /** * Renders device shield with the support of unknown shields instead of black shields which is used for rooms. - * @param roomEncryptionTrustLevel trust level that is usally calculated with [im.vector.app.features.settings.devices.TrustUtils.shieldForTrust] + * @param roomEncryptionTrustLevel trust level that is usually calculated with [im.vector.app.features.settings.devices.TrustUtils.shieldForTrust] * @param borderLess if true then the shield icon with border around is used */ fun renderDeviceShield(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, borderLess: Boolean = false) { - isVisible = roomEncryptionTrustLevel != null - - if (roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Default) { - contentDescription = context.getString(R.string.a11y_trust_level_default) - setImageResource( - if (borderLess) R.drawable.ic_shield_unknown_no_border - else R.drawable.ic_shield_unknown - ) - } else { - render(roomEncryptionTrustLevel, borderLess) + when (roomEncryptionTrustLevel) { + null -> { + contentDescription = context.getString(R.string.a11y_trust_level_warning) + setImageResource( + if (borderLess) R.drawable.ic_shield_warning_no_border + else R.drawable.ic_shield_warning + ) + } + RoomEncryptionTrustLevel.Default -> { + contentDescription = context.getString(R.string.a11y_trust_level_default) + setImageResource( + if (borderLess) R.drawable.ic_shield_unknown_no_border + else R.drawable.ic_shield_unknown + ) + } + else -> render(roomEncryptionTrustLevel, borderLess) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index 67b41ea5aac..e779948b411 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -88,7 +88,7 @@ data class DevicesViewState( data class DeviceFullInfo( val deviceInfo: DeviceInfo, val cryptoDeviceInfo: CryptoDeviceInfo?, - val trustLevelForShield: RoomEncryptionTrustLevel, + val trustLevelForShield: RoomEncryptionTrustLevel?, val isInactive: Boolean, ) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceFullInfo.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceFullInfo.kt index 4864c413940..186a6ebe692 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceFullInfo.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceFullInfo.kt @@ -25,7 +25,7 @@ import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel data class DeviceFullInfo( val deviceInfo: DeviceInfo, val cryptoDeviceInfo: CryptoDeviceInfo?, - val roomEncryptionTrustLevel: RoomEncryptionTrustLevel, + val roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, val isInactive: Boolean, val isCurrentDevice: Boolean, val deviceExtendedInfo: DeviceExtendedInfo, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt index 7727cee4fa3..eecec72b0a2 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt @@ -85,13 +85,14 @@ class SessionInfoView @JvmOverloads constructor( } private fun renderVerificationStatus( - encryptionTrustLevel: RoomEncryptionTrustLevel, + encryptionTrustLevel: RoomEncryptionTrustLevel?, isCurrentSession: Boolean, hasLearnMoreLink: Boolean, isVerifyButtonVisible: Boolean, ) { views.sessionInfoVerificationStatusImageView.renderDeviceShield(encryptionTrustLevel) when { + encryptionTrustLevel == null -> renderCrossSigningEncryptionNotSupported() encryptionTrustLevel == RoomEncryptionTrustLevel.Trusted -> renderCrossSigningVerified(isCurrentSession) encryptionTrustLevel == RoomEncryptionTrustLevel.Default && !isCurrentSession -> renderCrossSigningUnknown() else -> renderCrossSigningUnverified(isCurrentSession, isVerifyButtonVisible) @@ -149,6 +150,14 @@ class SessionInfoView @JvmOverloads constructor( views.sessionInfoVerifySessionButton.isVisible = false } + private fun renderCrossSigningEncryptionNotSupported() { + views.sessionInfoVerificationStatusTextView.text = context.getString(R.string.device_manager_verification_status_unverified) + views.sessionInfoVerificationStatusTextView.setTextColor(ThemeUtils.getColor(context, R.attr.colorError)) + views.sessionInfoVerificationStatusDetailTextView.text = + context.getString(R.string.device_manager_verification_status_detail_session_encryption_not_supported) + views.sessionInfoVerifySessionButton.isVisible = false + } + private fun renderDeviceInfo(sessionName: String, deviceType: DeviceType, stringProvider: StringProvider) { setDeviceTypeIconUseCase.execute(deviceType, views.sessionInfoDeviceTypeImageView, stringProvider) views.sessionInfoNameTextView.text = sessionName diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index be60b3b8055..6fbd2f1fef8 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -229,7 +229,7 @@ class SessionOverviewFragment : ) views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider, stringProvider) views.sessionOverviewInfo.onLearnMoreClickListener = { - showLearnMoreInfoVerificationStatus(deviceInfo.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Trusted) + showLearnMoreInfoVerificationStatus(deviceInfo.roomEncryptionTrustLevel) } } else { views.sessionOverviewInfo.isVisible = false @@ -293,21 +293,28 @@ class SessionOverviewFragment : } } - private fun showLearnMoreInfoVerificationStatus(isVerified: Boolean) { - val titleResId = if (isVerified) { - R.string.device_manager_verification_status_verified - } else { - R.string.device_manager_verification_status_unverified - } - val descriptionResId = if (isVerified) { - R.string.device_manager_learn_more_sessions_verified_description - } else { - R.string.device_manager_learn_more_sessions_unverified + private fun showLearnMoreInfoVerificationStatus(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?) { + val args = when(roomEncryptionTrustLevel) { + null -> { + // encryption not supported + SessionLearnMoreBottomSheet.Args( + title = getString(R.string.device_manager_verification_status_unverified), + description = getString(R.string.device_manager_learn_more_sessions_encryption_not_supported), + ) + } + RoomEncryptionTrustLevel.Trusted -> { + SessionLearnMoreBottomSheet.Args( + title = getString(R.string.device_manager_verification_status_verified), + description = getString(R.string.device_manager_learn_more_sessions_verified_description), + ) + } + else -> { + SessionLearnMoreBottomSheet.Args( + title = getString(R.string.device_manager_verification_status_unverified), + description = getString(R.string.device_manager_learn_more_sessions_unverified), + ) + } } - val args = SessionLearnMoreBottomSheet.Args( - title = getString(titleResId), - description = getString(descriptionResId), - ) SessionLearnMoreBottomSheet.show(childFragmentManager, args) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt index ba9a380ade8..31a7e93d045 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt @@ -25,11 +25,15 @@ class GetEncryptionTrustLevelForDeviceUseCase @Inject constructor( private val getEncryptionTrustLevelForOtherDeviceUseCase: GetEncryptionTrustLevelForOtherDeviceUseCase, ) { - fun execute(currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo, cryptoDeviceInfo: CryptoDeviceInfo?): RoomEncryptionTrustLevel { + fun execute(currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo, cryptoDeviceInfo: CryptoDeviceInfo?): RoomEncryptionTrustLevel? { + if(cryptoDeviceInfo == null) { + return null + } + val legacyMode = !currentSessionCrossSigningInfo.isCrossSigningInitialized val trustMSK = currentSessionCrossSigningInfo.isCrossSigningVerified - val isCurrentDevice = !cryptoDeviceInfo?.deviceId.isNullOrEmpty() && cryptoDeviceInfo?.deviceId == currentSessionCrossSigningInfo.deviceId - val deviceTrustLevel = cryptoDeviceInfo?.trustLevel + val isCurrentDevice = !cryptoDeviceInfo.deviceId.isNullOrEmpty() && cryptoDeviceInfo.deviceId == currentSessionCrossSigningInfo.deviceId + val deviceTrustLevel = cryptoDeviceInfo.trustLevel return when { isCurrentDevice -> getEncryptionTrustLevelForCurrentDeviceUseCase.execute(trustMSK, legacyMode) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCaseTest.kt index 1b39fe5f73d..fd10ee10837 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCaseTest.kt @@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices.v2.verification import io.mockk.every import io.mockk.mockk import io.mockk.verify +import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel @@ -89,6 +90,20 @@ class GetEncryptionTrustLevelForDeviceUseCaseTest { } } + @Test + fun `given no crypto device info when computing trust level then result is null`() { + val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo( + deviceId = A_DEVICE_ID, + isCrossSigningInitialized = true, + isCrossSigningVerified = false + ) + val cryptoDeviceInfo = null + + val result = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo) + + result shouldBe null + } + private fun givenCurrentSessionCrossSigningInfo( deviceId: String, isCrossSigningInitialized: Boolean, From a44c8dfca302bab2e50f349acdbf0ed4a419a325 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 7 Dec 2022 15:10:21 +0100 Subject: [PATCH 131/197] Renaming a method to avoid confusion --- .../settings/devices/v2/VectorSettingsDevicesFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index a21c7accb75..c21b044f1fb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -102,7 +102,7 @@ class VectorSettingsDevicesFragment : initWaitingView() initCurrentSessionHeaderView() - initCurrentSessionListView() + initCurrentSessionView() initOtherSessionsHeaderView() initOtherSessionsView() initSecurityRecommendationsView() @@ -177,7 +177,7 @@ class VectorSettingsDevicesFragment : activity?.let { SignOutUiWorker(it).perform() } } - private fun initCurrentSessionListView() { + private fun initCurrentSessionView() { views.deviceListCurrentSession.viewVerifyButton.debouncedClicks { viewModel.handle(DevicesAction.VerifyCurrentSession) } From 6c94f1cd52a6e871281a299ef92614dfeddb5cf2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Dec 2022 15:50:26 +0100 Subject: [PATCH 132/197] Quick tweak on the release script. --- tools/release/releaseScript.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh index d76cd980619..f91e11584c2 100755 --- a/tools/release/releaseScript.sh +++ b/tools/release/releaseScript.sh @@ -345,7 +345,8 @@ ${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86-release-signe printf "File vector-gplay-x86_64-release-signed.apk:\n" ${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86_64-release-signed.apk | grep package -read -p "\nDoes it look correct? Press enter when it's done." +printf "\n" +read -p "Does it look correct? Press enter when it's done." printf "\n================================================================================\n" read -p "Installing apk on a real device, press enter when a real device is connected. " @@ -356,7 +357,7 @@ read -p "Please run the APK on your phone to check that the upgrade went well (n # TODO Get the block to copy from towncrier earlier (be may be edited by the release manager)? read -p "Create the release on gitHub from the tag https://github.com/vector-im/element-android/tags, copy paste the block from the file CHANGES.md. Press enter when it's done." -read -p "Add the 4 signed APKs to the GitHub release. Press enter when it's done." +read -p "Add the 4 signed APKs to the GitHub release. They are located at ${targetPath}. Press enter when it's done." printf "\n================================================================================\n" printf "Message for the Android internal room:\n\n" From 88f743988064b51c1562b8c9291032f2e83639a8 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 7 Dec 2022 15:52:56 +0100 Subject: [PATCH 133/197] Updating comment to clarify intention --- .../app/features/home/UnknownDeviceDetectorSharedViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt index 21c7bd6ea1d..347c16653db 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -104,7 +104,7 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( // Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}") infoList .filter { info -> - // filter verified session, by checking the crypto device info + // filter out verified sessions or those which do not support encryption (i.e. without crypto info) cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse() } // filter out ignored devices From d244f7324c40c59b82c315491ad459591f006d81 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 7 Dec 2022 18:12:25 +0300 Subject: [PATCH 134/197] Add api functions to delete account data. --- .../android/sdk/internal/session/room/RoomAPI.kt | 13 +++++++++++++ .../session/user/accountdata/AccountDataAPI.kt | 13 +++++++++++++ 2 files changed, 26 insertions(+) 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 31bed90b622..3cf5526a478 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 @@ -427,6 +427,19 @@ internal interface RoomAPI { @Body content: JsonDict ) + /** + * Remove an account_data event from the room. + * @param userId the user id + * @param roomId the room id + * @param type the type + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "org.matrix.msc3391/user/{userId}/rooms/{roomId}/account_data/{type}") + suspend fun deleteRoomAccountData( + @Path("userId") userId: String, + @Path("roomId") roomId: String, + @Path("type") type: String + ) + /** * Upgrades the given room to a particular room version. * Errors: 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 b283d518458..fd813f1fedd 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 @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.user.accountdata import org.matrix.android.sdk.internal.network.NetworkConstants import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.PUT import retrofit2.http.Path @@ -36,4 +37,16 @@ internal interface AccountDataAPI { @Path("type") type: String, @Body params: Any ) + + /** + * Remove an account_data for the client. + * + * @param userId the user id + * @param type the type + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc3391/user/{userId}/account_data/{type}") + suspend fun deleteAccountData( + @Path("userId") userId: String, + @Path("type") type: String + ) } From 765202e05a841c257b771bb0d446e61f4f368b3f Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 7 Dec 2022 18:17:43 +0300 Subject: [PATCH 135/197] Add helper functions to delete user and room account data. --- .../database/model/RoomAccountDataEntity.kt | 5 ++- .../query/RoomAccountDataEntityQueries.kt | 33 +++++++++++++++++++ .../query/UserAccountDataEntityQueries.kt | 33 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserAccountDataEntityQueries.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt index 40040b57382..2eb5a637844 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt @@ -24,4 +24,7 @@ import io.realm.annotations.RealmClass internal open class RoomAccountDataEntity( @Index var type: String? = null, var contentStr: String? = null -) : RealmObject() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt new file mode 100644 index 00000000000..5ae4c5da72f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import io.realm.Realm +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntity +import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields + +/** + * Delete an account_data event. + */ +internal fun RoomAccountDataEntity.Companion.delete(realm: Realm, type: String) { + realm + .where() + .equalTo(RoomAccountDataEntityFields.TYPE, type) + .findFirst() + ?.deleteFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserAccountDataEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserAccountDataEntityQueries.kt new file mode 100644 index 00000000000..b28965aecad --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserAccountDataEntityQueries.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import io.realm.Realm +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity +import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields + +/** + * Delete an account_data event. + */ +internal fun UserAccountDataEntity.Companion.delete(realm: Realm, type: String) { + realm + .where() + .equalTo(UserAccountDataEntityFields.TYPE, type) + .findFirst() + ?.deleteFromRealm() +} From 23c2682f8d4e85466a90352f99bb9ad2b40630c0 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 7 Dec 2022 16:39:51 +0100 Subject: [PATCH 136/197] Fixing code style issues --- .../settings/devices/v2/overview/SessionOverviewFragment.kt | 2 +- .../v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index 6fbd2f1fef8..f3df0cced08 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -294,7 +294,7 @@ class SessionOverviewFragment : } private fun showLearnMoreInfoVerificationStatus(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?) { - val args = when(roomEncryptionTrustLevel) { + val args = when (roomEncryptionTrustLevel) { null -> { // encryption not supported SessionLearnMoreBottomSheet.Args( diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt index 31a7e93d045..268ae866013 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt @@ -26,7 +26,7 @@ class GetEncryptionTrustLevelForDeviceUseCase @Inject constructor( ) { fun execute(currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo, cryptoDeviceInfo: CryptoDeviceInfo?): RoomEncryptionTrustLevel? { - if(cryptoDeviceInfo == null) { + if (cryptoDeviceInfo == null) { return null } From f4429d4c9c9acdc4495e9e3fde098688626fe5f0 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 7 Dec 2022 18:58:14 +0300 Subject: [PATCH 137/197] Handle sync response to delete user and room account data. --- .../query/RoomAccountDataEntityQueries.kt | 33 ------------------- .../database/query/RoomEntityQueries.kt | 9 +++++ .../handler/UserAccountDataSyncHandler.kt | 21 +++++++----- .../parsing/RoomSyncAccountDataHandler.kt | 19 +++++++---- 4 files changed, 34 insertions(+), 48 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt deleted file mode 100644 index 5ae4c5da72f..00000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2022 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.database.query - -import io.realm.Realm -import io.realm.kotlin.where -import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntity -import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields - -/** - * Delete an account_data event. - */ -internal fun RoomAccountDataEntity.Companion.delete(realm: Realm, type: String) { - realm - .where() - .equalTo(RoomAccountDataEntityFields.TYPE, type) - .findFirst() - ?.deleteFromRealm() -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt index 08bb9e7ff32..0489fe690fd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt @@ -21,6 +21,7 @@ import io.realm.RealmQuery import io.realm.kotlin.where import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntityFields @@ -44,3 +45,11 @@ internal fun RoomEntity.Companion.where(realm: Realm, membership: Membership? = internal fun RoomEntity.fastContains(eventId: String): Boolean { return EventEntity.where(realm, eventId = eventId).findFirst() != null } + +internal fun RoomEntity.removeAccountData(type: String) { + accountData + .where() + .equalTo(RoomAccountDataEntityFields.TYPE, type) + .findFirst() + ?.deleteFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt index 0f296ded5d3..fb2dfa10f6a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt @@ -45,6 +45,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.deleteOnCascade +import org.matrix.android.sdk.internal.database.query.delete import org.matrix.android.sdk.internal.database.query.findAllFrom import org.matrix.android.sdk.internal.database.query.getDirectRooms import org.matrix.android.sdk.internal.database.query.getOrCreate @@ -81,20 +82,24 @@ internal class UserAccountDataSyncHandler @Inject constructor( fun handle(realm: Realm, accountData: UserAccountDataSync?) { accountData?.list?.forEach { event -> - // Generic handling, just save in base - handleGenericAccountData(realm, event.type, event.content) - when (event.type) { - UserAccountDataTypes.TYPE_DIRECT_MESSAGES -> handleDirectChatRooms(realm, event) - UserAccountDataTypes.TYPE_PUSH_RULES -> handlePushRules(realm, event) - UserAccountDataTypes.TYPE_IGNORED_USER_LIST -> handleIgnoredUsers(realm, event) - UserAccountDataTypes.TYPE_BREADCRUMBS -> handleBreadcrumbs(realm, event) + if (event.content.isEmpty()) { + UserAccountDataEntity.delete(realm, event.type) + } else { + // Generic handling, just save in base + handleGenericAccountData(realm, event.type, event.content) + when (event.type) { + UserAccountDataTypes.TYPE_DIRECT_MESSAGES -> handleDirectChatRooms(realm, event) + UserAccountDataTypes.TYPE_PUSH_RULES -> handlePushRules(realm, event) + UserAccountDataTypes.TYPE_IGNORED_USER_LIST -> handleIgnoredUsers(realm, event) + UserAccountDataTypes.TYPE_BREADCRUMBS -> handleBreadcrumbs(realm, event) + } } } } // If we get some direct chat invites, we synchronize the user account data including those. suspend fun synchronizeWithServerIfNeeded(invites: Map) { - if (invites.isNullOrEmpty()) return + if (invites.isEmpty()) return val directChats = directChatsHelper.getLocalDirectMessages().toMutable() var hasUpdate = false monarchy.doWithRealm { realm -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt index b1b2bfef339..c5f82940779 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt @@ -27,6 +27,7 @@ import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntity import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.removeAccountData import org.matrix.android.sdk.internal.session.room.read.FullyReadContent import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler import org.matrix.android.sdk.internal.session.sync.handler.room.RoomTagHandler @@ -44,13 +45,17 @@ internal class RoomSyncAccountDataHandler @Inject constructor( val roomEntity = RoomEntity.getOrCreate(realm, roomId) for (event in accountData.events) { val eventType = event.getClearType() - handleGeneric(roomEntity, event.getClearContent(), eventType) - if (eventType == RoomAccountDataTypes.EVENT_TYPE_TAG) { - val content = event.getClearContent().toModel() - roomTagHandler.handle(realm, roomId, content) - } else if (eventType == RoomAccountDataTypes.EVENT_TYPE_FULLY_READ) { - val content = event.getClearContent().toModel() - roomFullyReadHandler.handle(realm, roomId, content) + if (event.getClearContent().isNullOrEmpty()) { + roomEntity.removeAccountData(eventType) + } else { + handleGeneric(roomEntity, event.getClearContent(), eventType) + if (eventType == RoomAccountDataTypes.EVENT_TYPE_TAG) { + val content = event.getClearContent().toModel() + roomTagHandler.handle(realm, roomId, content) + } else if (eventType == RoomAccountDataTypes.EVENT_TYPE_FULLY_READ) { + val content = event.getClearContent().toModel() + roomFullyReadHandler.handle(realm, roomId, content) + } } } } From fdb8743ad36896dfef6760ae3d7402780dabe538 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 1 Dec 2022 16:14:21 +0100 Subject: [PATCH 138/197] Create provider package --- .../android/sdk/common/TestRoomDisplayNameFallbackProvider.kt | 2 +- .../main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt | 2 ++ .../api/{ => provider}/MatrixItemDisplayNameFallbackProvider.kt | 2 +- .../sdk/api/{ => provider}/RoomDisplayNameFallbackProvider.kt | 2 +- .../displayname/VectorMatrixItemDisplayNameFallbackProvider.kt | 2 +- .../app/features/room/VectorRoomDisplayNameFallbackProvider.kt | 2 +- 6 files changed, 7 insertions(+), 5 deletions(-) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/{ => provider}/MatrixItemDisplayNameFallbackProvider.kt (94%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/{ => provider}/RoomDisplayNameFallbackProvider.kt (97%) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt index af2d57f9ce2..a74f5010c28 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt @@ -16,7 +16,7 @@ package org.matrix.android.sdk.common -import org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider +import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index 00d74ab446a..d19fbe5049a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -20,6 +20,8 @@ import okhttp3.ConnectionSpec import okhttp3.Interceptor import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.metrics.MetricPlugin +import org.matrix.android.sdk.api.provider.MatrixItemDisplayNameFallbackProvider +import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider import java.net.Proxy data class MatrixConfiguration( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixItemDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/MatrixItemDisplayNameFallbackProvider.kt similarity index 94% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixItemDisplayNameFallbackProvider.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/MatrixItemDisplayNameFallbackProvider.kt index 82008cda8c6..971845eae7f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixItemDisplayNameFallbackProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/MatrixItemDisplayNameFallbackProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.api +package org.matrix.android.sdk.api.provider import org.matrix.android.sdk.api.util.MatrixItem diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt similarity index 97% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt index 3c376b55ee1..37d9b46b0ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.api +package org.matrix.android.sdk.api.provider /** * This interface exists to let the implementation provide localized room display name fallback. diff --git a/vector/src/main/java/im/vector/app/features/displayname/VectorMatrixItemDisplayNameFallbackProvider.kt b/vector/src/main/java/im/vector/app/features/displayname/VectorMatrixItemDisplayNameFallbackProvider.kt index 23b55335b80..77f08bf5784 100644 --- a/vector/src/main/java/im/vector/app/features/displayname/VectorMatrixItemDisplayNameFallbackProvider.kt +++ b/vector/src/main/java/im/vector/app/features/displayname/VectorMatrixItemDisplayNameFallbackProvider.kt @@ -16,7 +16,7 @@ package im.vector.app.features.displayname -import org.matrix.android.sdk.api.MatrixItemDisplayNameFallbackProvider +import org.matrix.android.sdk.api.provider.MatrixItemDisplayNameFallbackProvider import org.matrix.android.sdk.api.util.MatrixItem // Used to provide the fallback to the MatrixSDK, in the MatrixConfiguration diff --git a/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt b/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt index 118017861c2..cfbc2748adf 100644 --- a/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt +++ b/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt @@ -18,7 +18,7 @@ package im.vector.app.features.room import android.content.Context import im.vector.app.R -import org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider +import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider import javax.inject.Inject class VectorRoomDisplayNameFallbackProvider @Inject constructor( From 4d6c04baf9d713997b1061e6f9d94fe34d0ff00d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 1 Dec 2022 18:08:30 +0100 Subject: [PATCH 139/197] Add provider for custom event types --- .../android/sdk/api/MatrixConfiguration.kt | 8 +++-- .../api/provider/CustomEventTypesProvider.kt | 30 +++++++++++++++++++ .../room/summary/RoomSummaryEventsHelper.kt | 10 +++++-- .../room/summary/RoomSummaryUpdater.kt | 7 +++-- 4 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/CustomEventTypesProvider.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index d19fbe5049a..ccfe557ef60 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -20,6 +20,7 @@ import okhttp3.ConnectionSpec import okhttp3.Interceptor import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.metrics.MetricPlugin +import org.matrix.android.sdk.api.provider.CustomEventTypesProvider import org.matrix.android.sdk.api.provider.MatrixItemDisplayNameFallbackProvider import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider import java.net.Proxy @@ -77,9 +78,12 @@ data class MatrixConfiguration( * Sync configuration. */ val syncConfig: SyncConfig = SyncConfig(), - /** * Metrics plugin that can be used to capture metrics from matrix-sdk-android. */ - val metricPlugins: List = emptyList() + val metricPlugins: List = emptyList(), + /** + * CustomEventTypesProvider to provide custom event types to the sdk which should be processed with internal events. + */ + val customEventTypesProvider: CustomEventTypesProvider? = null, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/CustomEventTypesProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/CustomEventTypesProvider.kt new file mode 100644 index 00000000000..c0f66dc1c2d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/CustomEventTypesProvider.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.provider + +import org.matrix.android.sdk.api.session.room.model.RoomSummary + +/** + * Provide custom event types which should be processed with the internal event types. + */ +interface CustomEventTypesProvider { + + /** + * Custom event types to include when computing [RoomSummary.latestPreviewableEvent]. + */ + val customPreviewableEventTypes: List +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt index 7437a686da8..a68ae620dc3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt @@ -17,17 +17,23 @@ package org.matrix.android.sdk.internal.session.room.summary import io.realm.Realm +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.room.summary.RoomSummaryConstants import org.matrix.android.sdk.api.session.room.timeline.EventTypeFilter import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.latestEvent +import javax.inject.Inject -internal object RoomSummaryEventsHelper { +internal class RoomSummaryEventsHelper @Inject constructor( + matrixConfiguration: MatrixConfiguration, +) { private val previewFilters = TimelineEventFilters( filterTypes = true, - allowedTypes = RoomSummaryConstants.PREVIEWABLE_TYPES.map { EventTypeFilter(eventType = it, stateKey = null) }, + allowedTypes = RoomSummaryConstants.PREVIEWABLE_TYPES + .plus(matrixConfiguration.customEventTypesProvider?.customPreviewableEventTypes.orEmpty()) + .map { EventTypeFilter(eventType = it, stateKey = null) }, filterUseless = true, filterRedacted = false, filterEdits = true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 21a0862c652..69beb8d599a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -78,12 +78,13 @@ internal class RoomSummaryUpdater @Inject constructor( private val crossSigningService: DefaultCrossSigningService, private val roomAccountDataDataSource: RoomAccountDataDataSource, private val homeServerCapabilitiesService: HomeServerCapabilitiesService, + private val roomSummaryEventsHelper: RoomSummaryEventsHelper, ) { fun refreshLatestPreviewContent(realm: Realm, roomId: String) { val roomSummaryEntity = RoomSummaryEntity.getOrNull(realm, roomId) if (roomSummaryEntity != null) { - val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) + val latestPreviewableEvent = roomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) latestPreviewableEvent?.attemptToDecrypt() } } @@ -145,7 +146,7 @@ internal class RoomSummaryUpdater @Inject constructor( val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root Timber.d("## CRYPTO: currentEncryptionEvent is $encryptionEvent") - val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) + val latestPreviewableEvent = roomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) val lastActivityFromEvent = latestPreviewableEvent?.root?.originServerTs if (lastActivityFromEvent != null) { @@ -231,7 +232,7 @@ internal class RoomSummaryUpdater @Inject constructor( fun updateSendingInformation(realm: Realm, roomId: String) { val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) roomSummaryEntity.updateHasFailedSending() - roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) + roomSummaryEntity.latestPreviewableEvent = roomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) } /** From 6e5461f300d4724aa853cfa4ee849f61262b80cb Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 2 Dec 2022 17:24:40 +0100 Subject: [PATCH 140/197] Stop filtering events with reference relationship when computing latest previewable event --- .../sdk/internal/database/query/TimelineEventEntityQueries.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt index 1b4b3599165..37df901c7d9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt @@ -115,7 +115,6 @@ internal fun RealmQuery.filterEvents(filters: TimelineEvent if (filters.filterEdits) { not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) - not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.REFERENCE) } if (filters.filterRedacted) { not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED) From 1a3ca7b1a06b943fe9a36e53f7e5cee3d145cdff Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 2 Dec 2022 17:25:54 +0100 Subject: [PATCH 141/197] Filter event types from decrypted content --- .../query/TimelineEventEntityQueries.kt | 43 ++++++++++++++++--- .../database/query/TimelineEventFilter.kt | 1 + 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt index 37df901c7d9..ab90801b7ff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt @@ -22,6 +22,7 @@ import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort import io.realm.kotlin.where +import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters import org.matrix.android.sdk.internal.database.model.ChunkEntity @@ -94,14 +95,27 @@ internal fun RealmQuery.filterEvents(filters: TimelineEvent if (filters.filterTypes && filters.allowedTypes.isNotEmpty()) { beginGroup() filters.allowedTypes.forEachIndexed { index, filter -> - if (filter.stateKey == null) { - equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType) + if (filter.eventType == EventType.ENCRYPTED) { + val otherTypes = filters.allowedTypes.minus(filter).map { it.eventType } + if (filter.stateKey == null) { + filterEncryptedTypes(otherTypes) + } else { + beginGroup() + filterEncryptedTypes(otherTypes) + and() + equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, filter.stateKey) + endGroup() + } } else { - beginGroup() - equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType) - and() - equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, filter.stateKey) - endGroup() + if (filter.stateKey == null) { + equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType) + } else { + beginGroup() + equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType) + and() + equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, filter.stateKey) + endGroup() + } } if (index != filters.allowedTypes.size - 1) { or() @@ -123,6 +137,21 @@ internal fun RealmQuery.filterEvents(filters: TimelineEvent return this } +internal fun RealmQuery.filterEncryptedTypes(allowedTypes: List): RealmQuery { + beginGroup() + equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.ENCRYPTED) + and() + beginGroup() + isNull(TimelineEventEntityFields.ROOT.DECRYPTION_RESULT_JSON) + allowedTypes.forEach { eventType -> + or() + like(TimelineEventEntityFields.ROOT.DECRYPTION_RESULT_JSON, TimelineEventFilter.DecryptedContent.type(eventType)) + } + endGroup() + endGroup() + return this +} + internal fun RealmQuery.filterTypes(filterTypes: List): RealmQuery { return if (filterTypes.isEmpty()) { this diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt index 7a65623b76f..b8baeb0b33d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt @@ -34,6 +34,7 @@ internal object TimelineEventFilter { */ internal object DecryptedContent { internal const val URL = """{*"file":*"url":*}""" + fun type(type: String) = """{*"type":*"$type"*}""" } /** From 69beef464871e77cccb41ec0928da109262d5278 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 1 Dec 2022 18:15:14 +0100 Subject: [PATCH 142/197] Show voice broadcast events in the room list fix factory --- .../src/main/res/values/strings.xml | 3 ++ .../im/vector/app/core/di/SingletonModule.kt | 3 ++ .../VectorCustomEventTypesProvider.kt | 28 +++++++++++++++++++ .../format/DisplayableEventFormatter.kt | 26 +++++++++++++++++ .../home/room/list/RoomSummaryItemFactory.kt | 23 +++++++++++++-- .../GetOngoingVoiceBroadcastsUseCase.kt | 4 +-- 6 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/configuration/VectorCustomEventTypesProvider.kt diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 7f11e63469f..2d289150c63 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -134,6 +134,8 @@ ** Unable to decrypt: %s ** The sender\'s device has not sent us the keys for this message. + %1$s ended a voice broadcast. + @@ -3101,6 +3103,7 @@ (%1$s) Live + Live broadcast Buffering… Resume voice broadcast record diff --git a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt index 28ca761acef..7a3aa7cf8b8 100644 --- a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -48,6 +48,7 @@ import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.impl.DefaultVectorAnalytics import im.vector.app.features.analytics.metrics.VectorPlugins +import im.vector.app.features.configuration.VectorCustomEventTypesProvider import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.CompileTimeAutoAcceptInvites import im.vector.app.features.navigation.DefaultNavigator @@ -141,6 +142,7 @@ import javax.inject.Singleton vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider, flipperProxy: FlipperProxy, vectorPlugins: VectorPlugins, + vectorCustomEventTypesProvider: VectorCustomEventTypesProvider, ): MatrixConfiguration { return MatrixConfiguration( applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION, @@ -150,6 +152,7 @@ import javax.inject.Singleton flipperProxy.networkInterceptor(), ), metricPlugins = vectorPlugins.plugins(), + customEventTypesProvider = vectorCustomEventTypesProvider, ) } diff --git a/vector/src/main/java/im/vector/app/features/configuration/VectorCustomEventTypesProvider.kt b/vector/src/main/java/im/vector/app/features/configuration/VectorCustomEventTypesProvider.kt new file mode 100644 index 00000000000..55244685d77 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/configuration/VectorCustomEventTypesProvider.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.configuration + +import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import org.matrix.android.sdk.api.provider.CustomEventTypesProvider +import javax.inject.Inject + +class VectorCustomEventTypesProvider @Inject constructor() : CustomEventTypesProvider { + + override val customPreviewableEventTypes = listOf( + VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO + ) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index aaa0fc10c9b..c8af85db4f0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -21,8 +21,14 @@ import im.vector.app.EmojiSpanify import im.vector.app.R import im.vector.app.core.extensions.getVectorLastMessageContent import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.html.EventHtmlRenderer +import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import im.vector.app.features.voicebroadcast.isLive +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import me.gujun.android.span.image import me.gujun.android.span.span import org.commonmark.node.Document import org.matrix.android.sdk.api.session.events.model.Event @@ -41,6 +47,7 @@ import javax.inject.Inject class DisplayableEventFormatter @Inject constructor( private val stringProvider: StringProvider, private val colorProvider: ColorProvider, + private val drawableProvider: DrawableProvider, private val emojiSpanify: EmojiSpanify, private val noticeEventFormatter: NoticeEventFormatter, private val htmlRenderer: Lazy @@ -135,6 +142,9 @@ class DisplayableEventFormatter @Inject constructor( in EventType.STATE_ROOM_BEACON_INFO.values -> { simpleFormat(senderName, stringProvider.getString(R.string.sent_live_location), appendAuthor) } + VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> { + formatVoiceBroadcastEvent(timelineEvent.root.asVoiceBroadcastEvent(), senderName) + } else -> { span { text = noticeEventFormatter.format(timelineEvent, isDm) ?: "" @@ -252,4 +262,20 @@ class DisplayableEventFormatter @Inject constructor( body } } + + private fun formatVoiceBroadcastEvent(voiceBroadcastEvent: VoiceBroadcastEvent?, senderName: String): CharSequence { + return if (voiceBroadcastEvent?.isLive == true) { + span { + drawableProvider.getDrawable(R.drawable.ic_voice_broadcast, colorProvider.getColor(R.color.palette_vermilion))?.let { + image(it) + +" " + } + span(stringProvider.getString(R.string.voice_broadcast_live_broadcast)) { + textColor = colorProvider.getColor(R.color.palette_vermilion) + } + } + } else { + stringProvider.getString(R.string.notice_voice_broadcast_ended, senderName) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index 638e3c185d4..ca80530261c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.Loading import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider @@ -29,21 +30,30 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.typing.TypingHelper +import im.vector.app.features.voicebroadcast.isVoiceBroadcast +import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState 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.SpaceChildInfo +import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject class RoomSummaryItemFactory @Inject constructor( + private val sessionHolder: ActiveSessionHolder, private val displayableEventFormatter: DisplayableEventFormatter, private val dateFormatter: VectorDateFormatter, private val stringProvider: StringProvider, private val typingHelper: TypingHelper, private val avatarRenderer: AvatarRenderer, - private val errorFormatter: ErrorFormatter + private val errorFormatter: ErrorFormatter, + private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, ) { fun create( @@ -129,7 +139,7 @@ class RoomSummaryItemFactory @Inject constructor( val showSelected = selectedRoomIds.contains(roomSummary.roomId) var latestFormattedEvent: CharSequence = "" var latestEventTime = "" - val latestEvent = roomSummary.latestPreviewableEvent + val latestEvent = roomSummary.getVectorLatestPreviewableEvent() if (latestEvent != null) { latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not()) latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST) @@ -225,4 +235,13 @@ class RoomSummaryItemFactory @Inject constructor( else -> stringProvider.getQuantityString(R.plurals.search_space_multiple_parents, size - 1, directParentNames[0], size - 1) } } + + private fun RoomSummary.getVectorLatestPreviewableEvent(): TimelineEvent? { + val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return latestPreviewableEvent + val liveVoiceBroadcastTimelineEvent = getOngoingVoiceBroadcastsUseCase.execute(roomId).lastOrNull() + ?.root?.eventId?.let { room.getTimelineEvent(it) } + return liveVoiceBroadcastTimelineEvent + ?: latestPreviewableEvent + ?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast + } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt index ec50618969b..9974db470f1 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt @@ -18,8 +18,8 @@ package im.vector.app.features.voicebroadcast.usecase import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import im.vector.app.features.voicebroadcast.isLive import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent -import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.getRoom @@ -44,6 +44,6 @@ class GetOngoingVoiceBroadcastsUseCase @Inject constructor( QueryStringValue.IsNotEmpty ) .mapNotNull { it.asVoiceBroadcastEvent() } - .filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } + .filter { it.isLive } } } From aa5270760e3cbc0b1915a08ab411c733536fb9c7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 5 Dec 2022 18:04:54 +0100 Subject: [PATCH 143/197] Hide typing events if there is a live voice broadcast --- .../app/features/home/room/list/RoomSummaryItemFactory.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index ca80530261c..d48448b4805 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -146,6 +146,8 @@ class RoomSummaryItemFactory @Inject constructor( } val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers) + // Skip typing while there is a live voice broadcast + .takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }.orEmpty() return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) { createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick) From 7a1dfef6d59d49d485883fbdbde794cb140e12f6 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 5 Dec 2022 18:42:24 +0100 Subject: [PATCH 144/197] Display a notice in the timeline when a voice broadcast is stopped --- .../src/main/res/values/strings.xml | 1 + .../format/DisplayableEventFormatter.kt | 10 ++++----- .../timeline/format/NoticeEventFormatter.kt | 21 +++++++++++++++++-- .../helper/TimelineEventVisibilityHelper.kt | 2 +- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 2d289150c63..127d63f74c8 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -135,6 +135,7 @@ The sender\'s device has not sent us the keys for this message. %1$s ended a voice broadcast. + You ended a voice broadcast. diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index c8af85db4f0..5fa9576dd41 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -20,13 +20,13 @@ import dagger.Lazy import im.vector.app.EmojiSpanify import im.vector.app.R import im.vector.app.core.extensions.getVectorLastMessageContent +import im.vector.app.core.extensions.orEmpty import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.isLive -import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import me.gujun.android.span.image import me.gujun.android.span.span @@ -143,7 +143,7 @@ class DisplayableEventFormatter @Inject constructor( simpleFormat(senderName, stringProvider.getString(R.string.sent_live_location), appendAuthor) } VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> { - formatVoiceBroadcastEvent(timelineEvent.root.asVoiceBroadcastEvent(), senderName) + formatVoiceBroadcastEvent(timelineEvent.root, isDm, senderName) } else -> { span { @@ -263,8 +263,8 @@ class DisplayableEventFormatter @Inject constructor( } } - private fun formatVoiceBroadcastEvent(voiceBroadcastEvent: VoiceBroadcastEvent?, senderName: String): CharSequence { - return if (voiceBroadcastEvent?.isLive == true) { + private fun formatVoiceBroadcastEvent(event: Event, isDm: Boolean, senderName: String): CharSequence { + return if (event.asVoiceBroadcastEvent()?.isLive == true) { span { drawableProvider.getDrawable(R.drawable.ic_voice_broadcast, colorProvider.getColor(R.color.palette_vermilion))?.let { image(it) @@ -275,7 +275,7 @@ class DisplayableEventFormatter @Inject constructor( } } } else { - stringProvider.getString(R.string.notice_voice_broadcast_ended, senderName) + noticeEventFormatter.format(event, senderName, isDm).orEmpty() } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 3f702ed72dc..b02e5157749 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -22,6 +22,8 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.roomprofile.permissions.RoleFormatter import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.extensions.appendNl import org.matrix.android.sdk.api.extensions.orFalse @@ -91,6 +93,9 @@ class NoticeEventFormatter @Inject constructor( EventType.CALL_HANGUP, EventType.CALL_REJECT, EventType.CALL_ANSWER -> formatCallEvent(type, timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> { + formatVoiceBroadcastEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + } EventType.CALL_NEGOTIATE, EventType.CALL_SELECT_ANSWER, EventType.CALL_REPLACES, @@ -109,8 +114,7 @@ class NoticeEventFormatter @Inject constructor( EventType.STICKER, in EventType.POLL_RESPONSE.values, in EventType.POLL_END.values, - in EventType.BEACON_LOCATION_DATA.values, - VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> formatDebug(timelineEvent.root) + in EventType.BEACON_LOCATION_DATA.values -> formatDebug(timelineEvent.root) else -> { Timber.v("Type $type not handled by this formatter") null @@ -191,6 +195,7 @@ class NoticeEventFormatter @Inject constructor( EventType.CALL_REJECT, EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName) EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, isDm) + VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> formatVoiceBroadcastEvent(event, senderName) else -> { Timber.v("Type $type not handled by this formatter") null @@ -894,4 +899,16 @@ class NoticeEventFormatter @Inject constructor( } } } + + private fun formatVoiceBroadcastEvent(event: Event, senderName: String?): CharSequence { + return if (event.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED) { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_voice_broadcast_ended_by_you) + } else { + sp.getString(R.string.notice_voice_broadcast_ended, senderName) + } + } else { + formatDebug(event) + } + } } 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 382f1c23015..703a5cb9114 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 @@ -252,7 +252,7 @@ class TimelineEventVisibilityHelper @Inject constructor( } if (root.getClearType() == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO && - root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState != VoiceBroadcastState.STARTED) { + root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState !in arrayOf(VoiceBroadcastState.STARTED, VoiceBroadcastState.STOPPED)) { return true } From 35c528405d367eca846b8f775404b2ccf570586e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 7 Dec 2022 10:15:49 +0100 Subject: [PATCH 145/197] Code cleanup --- .../timeline/format/NoticeEventFormatter.kt | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index b02e5157749..a306dd6b2fb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -69,33 +69,32 @@ class NoticeEventFormatter @Inject constructor( private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId fun format(timelineEvent: TimelineEvent, isDm: Boolean): CharSequence? { - return when (val type = timelineEvent.root.getClearType()) { - EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) - EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, isDm) - EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) - EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) - EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) - EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) - EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) - EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) - EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> - formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) - EventType.STATE_ROOM_SERVER_ACL -> formatRoomServerAclEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) - EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) - EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + val event = timelineEvent.root + val senderName = timelineEvent.senderInfo.disambiguatedDisplayName + return when (val type = event.getClearType()) { + EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName, isDm) + EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(event, isDm) + EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName) + EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName) + EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(event, senderName) + EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName, isDm) + EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName, isDm) + EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(event, senderName) + EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(event, senderName) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName, isDm) + EventType.STATE_ROOM_SERVER_ACL -> formatRoomServerAclEvent(event, senderName) + EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(event, senderName, isDm) + EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(event, senderName) EventType.STATE_ROOM_WIDGET, - EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) - EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) - EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(event, senderName) + EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, isDm) + EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(event, senderName) EventType.CALL_INVITE, EventType.CALL_CANDIDATES, EventType.CALL_HANGUP, EventType.CALL_REJECT, - EventType.CALL_ANSWER -> formatCallEvent(type, timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) - VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> { - formatVoiceBroadcastEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) - } + EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName) + VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> formatVoiceBroadcastEvent(event, senderName) EventType.CALL_NEGOTIATE, EventType.CALL_SELECT_ANSWER, EventType.CALL_REPLACES, @@ -114,7 +113,7 @@ class NoticeEventFormatter @Inject constructor( EventType.STICKER, in EventType.POLL_RESPONSE.values, in EventType.POLL_END.values, - in EventType.BEACON_LOCATION_DATA.values -> formatDebug(timelineEvent.root) + in EventType.BEACON_LOCATION_DATA.values -> formatDebug(event) else -> { Timber.v("Type $type not handled by this formatter") null From 28c59e3290898b8c3efb29ed9448db94c82842ae Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 6 Dec 2022 10:31:54 +0100 Subject: [PATCH 146/197] Changelog --- changelog.d/7719.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7719.feature diff --git a/changelog.d/7719.feature b/changelog.d/7719.feature new file mode 100644 index 00000000000..34df6ad964f --- /dev/null +++ b/changelog.d/7719.feature @@ -0,0 +1 @@ +Voice Broadcast - Update last message in the room list From bb7323a93593cf3940fc6323d83c2ee3ed6a8361 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 6 Dec 2022 17:24:52 +0100 Subject: [PATCH 147/197] Rename some use cases --- .../src/main/java/im/vector/app/core/di/VoiceModule.kt | 6 +++--- .../features/home/room/list/RoomSummaryItemFactory.kt | 8 +++++--- .../listening/VoiceBroadcastPlayerImpl.kt | 4 ++-- .../usecase/GetLiveVoiceBroadcastChunksUseCase.kt | 4 ++-- .../recording/VoiceBroadcastRecorderQ.kt | 4 ++-- .../recording/usecase/StartVoiceBroadcastUseCase.kt | 6 +++--- .../usecase/StopOngoingVoiceBroadcastUseCase.kt | 6 +++--- ...UseCase.kt => GetRoomLiveVoiceBroadcastsUseCase.kt} | 10 ++-------- ...se.kt => GetVoiceBroadcastStateEventLiveUseCase.kt} | 2 +- .../usecase/StartVoiceBroadcastUseCaseTest.kt | 6 +++--- 10 files changed, 26 insertions(+), 30 deletions(-) rename vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/{GetOngoingVoiceBroadcastsUseCase.kt => GetRoomLiveVoiceBroadcastsUseCase.kt} (84%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/{GetMostRecentVoiceBroadcastStateEventUseCase.kt => GetVoiceBroadcastStateEventLiveUseCase.kt} (99%) diff --git a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt index 6437326294b..40fb4ecea75 100644 --- a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt @@ -27,7 +27,7 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayerImpl import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorderQ -import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase +import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventLiveUseCase import javax.inject.Singleton @InstallIn(SingletonComponent::class) @@ -40,13 +40,13 @@ abstract class VoiceModule { fun providesVoiceBroadcastRecorder( context: Context, sessionHolder: ActiveSessionHolder, - getMostRecentVoiceBroadcastStateEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase, + getVoiceBroadcastStateEventLiveUseCase: GetVoiceBroadcastStateEventLiveUseCase, ): VoiceBroadcastRecorder? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { VoiceBroadcastRecorderQ( context = context, sessionHolder = sessionHolder, - getVoiceBroadcastEventUseCase = getMostRecentVoiceBroadcastStateEventUseCase + getVoiceBroadcastEventUseCase = getVoiceBroadcastStateEventLiveUseCase ) } else { null diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index d48448b4805..d8b7a427f06 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -30,8 +30,10 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.typing.TypingHelper +import im.vector.app.features.voicebroadcast.isLive import im.vector.app.features.voicebroadcast.isVoiceBroadcast -import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.getRoom @@ -53,7 +55,7 @@ class RoomSummaryItemFactory @Inject constructor( private val typingHelper: TypingHelper, private val avatarRenderer: AvatarRenderer, private val errorFormatter: ErrorFormatter, - private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, + private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase, ) { fun create( @@ -240,7 +242,7 @@ class RoomSummaryItemFactory @Inject constructor( private fun RoomSummary.getVectorLatestPreviewableEvent(): TimelineEvent? { val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return latestPreviewableEvent - val liveVoiceBroadcastTimelineEvent = getOngoingVoiceBroadcastsUseCase.execute(roomId).lastOrNull() + val liveVoiceBroadcastTimelineEvent = getRoomLiveVoiceBroadcastsUseCase.execute(roomId).lastOrNull() ?.root?.eventId?.let { room.getTimelineEvent(it) } return liveVoiceBroadcastTimelineEvent ?: latestPreviewableEvent diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index f8025d078ee..1bc3078c8b4 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -31,7 +31,7 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Stat import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent -import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase +import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventLiveUseCase import im.vector.lib.core.utils.timer.CountUpTimer import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn @@ -48,7 +48,7 @@ import javax.inject.Singleton class VoiceBroadcastPlayerImpl @Inject constructor( private val sessionHolder: ActiveSessionHolder, private val playbackTracker: AudioMessagePlaybackTracker, - private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase, + private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastStateEventLiveUseCase, private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase ) : VoiceBroadcastPlayer { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt index 03e713eeaad..b2aebd9932f 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -24,7 +24,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.sequence -import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase +import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventLiveUseCase import im.vector.app.features.voicebroadcast.voiceBroadcastId import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -48,7 +48,7 @@ import javax.inject.Inject */ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, - private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase, + private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastStateEventLiveUseCase, ) { fun execute(voiceBroadcast: VoiceBroadcast): Flow> { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt index b751417ca63..2da807293fe 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt @@ -26,7 +26,7 @@ import im.vector.app.features.voice.AbstractVoiceRecorderQ import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState -import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase +import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventLiveUseCase import im.vector.lib.core.utils.timer.CountUpTimer import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn @@ -40,7 +40,7 @@ import java.util.concurrent.TimeUnit class VoiceBroadcastRecorderQ( context: Context, private val sessionHolder: ActiveSessionHolder, - private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase + private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastStateEventLiveUseCase ) : AbstractVoiceRecorderQ(context), VoiceBroadcastRecorder { private val session get() = sessionHolder.getActiveSession() diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index e3814608ea8..87ea49cece7 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -28,7 +28,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder -import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase +import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase import im.vector.lib.multipicker.utils.toMultiPickerAudioType import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -56,7 +56,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( private val voiceBroadcastRecorder: VoiceBroadcastRecorder?, private val context: Context, private val buildMeta: BuildMeta, - private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, + private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase, private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase, ) { @@ -152,7 +152,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast") throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting } - getOngoingVoiceBroadcastsUseCase.execute(room.roomId).isNotEmpty() -> { + getRoomLiveVoiceBroadcastsUseCase.execute(room.roomId).isNotEmpty() -> { Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: user already broadcasting") throw VoiceBroadcastFailure.RecordingError.BlockedBySomeoneElse } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt index 791409b8698..fdbf1a067d4 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt @@ -19,7 +19,7 @@ package im.vector.app.features.voicebroadcast.recording.usecase import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent -import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase +import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.model.Membership @@ -32,7 +32,7 @@ import javax.inject.Inject */ class StopOngoingVoiceBroadcastUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, - private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, + private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase, private val voiceBroadcastHelper: VoiceBroadcastHelper, ) { @@ -53,7 +53,7 @@ class StopOngoingVoiceBroadcastUseCase @Inject constructor( recentRooms .forEach { room -> - val ongoingVoiceBroadcasts = getOngoingVoiceBroadcastsUseCase.execute(room.roomId) + val ongoingVoiceBroadcasts = getRoomLiveVoiceBroadcastsUseCase.execute(room.roomId) val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() } if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt similarity index 84% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt index 9974db470f1..fa5f06bfe64 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt @@ -23,22 +23,16 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.getRoom -import timber.log.Timber import javax.inject.Inject -class GetOngoingVoiceBroadcastsUseCase @Inject constructor( +class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, ) { fun execute(roomId: String): List { - val session = activeSessionHolder.getSafeActiveSession() ?: run { - Timber.d("## GetOngoingVoiceBroadcastsUseCase: no active session") - return emptyList() - } + val session = activeSessionHolder.getSafeActiveSession() ?: return emptyList() val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") - Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId") - return room.stateService().getStateEvents( setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO), QueryStringValue.IsNotEmpty diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt similarity index 99% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt index e0179e403f0..b3bbdad6355 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt @@ -42,7 +42,7 @@ import org.matrix.android.sdk.flow.mapOptional import timber.log.Timber import javax.inject.Inject -class GetMostRecentVoiceBroadcastStateEventUseCase @Inject constructor( +class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor( private val session: Session, ) { diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt index 5b4076378c8..5dfdd379e0b 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt @@ -52,14 +52,14 @@ class StartVoiceBroadcastUseCaseTest { private val fakeRoom = FakeRoom() private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom)) private val fakeVoiceBroadcastRecorder = mockk(relaxed = true) - private val fakeGetOngoingVoiceBroadcastsUseCase = mockk() + private val fakeGetRoomLiveVoiceBroadcastsUseCase = mockk() private val startVoiceBroadcastUseCase = spyk( StartVoiceBroadcastUseCase( session = fakeSession, voiceBroadcastRecorder = fakeVoiceBroadcastRecorder, context = FakeContext().instance, buildMeta = mockk(), - getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase, + getRoomLiveVoiceBroadcastsUseCase = fakeGetRoomLiveVoiceBroadcastsUseCase, stopVoiceBroadcastUseCase = mockk() ) ) @@ -140,7 +140,7 @@ class StartVoiceBroadcastUseCaseTest { } .mapNotNull { it.asVoiceBroadcastEvent() } .filter { it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } - every { fakeGetOngoingVoiceBroadcastsUseCase.execute(any()) } returns events + every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(any()) } returns events } private data class VoiceBroadcast(val userId: String, val state: VoiceBroadcastState) From 59859ec02ee1eca0a82c41fcb1c1d600f90b0701 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 7 Dec 2022 09:56:33 +0100 Subject: [PATCH 148/197] Prioritize call events against live broadcast --- .../app/features/home/room/list/RoomSummaryItemFactory.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index d8b7a427f06..a55900a5c4a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -36,6 +36,7 @@ import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState @@ -244,7 +245,8 @@ class RoomSummaryItemFactory @Inject constructor( val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return latestPreviewableEvent val liveVoiceBroadcastTimelineEvent = getRoomLiveVoiceBroadcastsUseCase.execute(roomId).lastOrNull() ?.root?.eventId?.let { room.getTimelineEvent(it) } - return liveVoiceBroadcastTimelineEvent + return latestPreviewableEvent?.takeIf { it.root.getClearType() == EventType.CALL_INVITE } + ?: liveVoiceBroadcastTimelineEvent ?: latestPreviewableEvent ?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast } From 055bf6d3029d8748a9f14ff4de5a24868bb4a2a3 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 7 Dec 2022 21:41:22 +0300 Subject: [PATCH 149/197] Revert unused companion object. --- .../sdk/internal/database/model/RoomAccountDataEntity.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt index 2eb5a637844..40040b57382 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt @@ -24,7 +24,4 @@ import io.realm.annotations.RealmClass internal open class RoomAccountDataEntity( @Index var type: String? = null, var contentStr: String? = null -) : RealmObject() { - - companion object -} +) : RealmObject() From a5ab1b4a8bdf10bb61a0c2966c63dc6be8837283 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Dec 2022 10:34:08 +0100 Subject: [PATCH 150/197] Fix crash `kotlin.UninitializedPropertyAccessException: lateinit property avatarRenderer has not been initialized`. AvatarRenderer is not used here. --- .../im/vector/app/features/userdirectory/InviteByEmailItem.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt index eaeec357912..da1d76e86ab 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt @@ -25,12 +25,10 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.features.home.AvatarRenderer @EpoxyModelClass abstract class InviteByEmailItem : VectorEpoxyModel(R.layout.item_invite_by_mail) { - @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var foundItem: ThreePidUser @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var clickListener: ClickListener? = null @EpoxyAttribute var selected: Boolean = false From 7034d822594a5d7417444c7d613893565e5c6ae9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Dec 2022 10:36:29 +0100 Subject: [PATCH 151/197] changelog --- changelog.d/7744.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7744.bugfix diff --git a/changelog.d/7744.bugfix b/changelog.d/7744.bugfix new file mode 100644 index 00000000000..7ed82a9c1c6 --- /dev/null +++ b/changelog.d/7744.bugfix @@ -0,0 +1 @@ +Fix crash when inviting by email. From b49045ff15044202a4ac104c39d2ebbf6eae53df Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 8 Dec 2022 10:37:00 +0100 Subject: [PATCH 152/197] Adding changelog entry --- changelog.d/7743.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7743.bugfix diff --git a/changelog.d/7743.bugfix b/changelog.d/7743.bugfix new file mode 100644 index 00000000000..867c12a3c30 --- /dev/null +++ b/changelog.d/7743.bugfix @@ -0,0 +1 @@ +Verification request is not showing when verify session popup is displayed From b25f185d63b5de9e1cb5523650e7aa7b5623cdc5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Dec 2022 10:48:17 +0100 Subject: [PATCH 153/197] Try to fix issue about danger file not found. --- .github/workflows/danger.yml | 2 +- .github/workflows/quality.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index e5226d07235..8752f339bdc 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -13,7 +13,7 @@ jobs: - name: Danger uses: danger/danger-js@11.2.0 with: - args: "--dangerfile tools/danger/dangerfile.js" + args: "--dangerfile ./tools/danger/dangerfile.js" env: DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} # Fallback for forks diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 57dd5a6a457..fae8d97688e 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -68,7 +68,7 @@ jobs: if: always() uses: danger/danger-js@11.2.0 with: - args: "--dangerfile tools/danger/dangerfile-lint.js" + args: "--dangerfile ./tools/danger/dangerfile-lint.js" env: DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} # Fallback for forks From 72ecd1bbc9adcf9fa01f2ed3cfa8e7b86af349a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Dec 2022 10:51:20 +0100 Subject: [PATCH 154/197] Bump kotlin-gradle-plugin from 1.7.21 to 1.7.22 (#7664) Bumps [kotlin-gradle-plugin](https://github.com/JetBrains/kotlin) from 1.7.21 to 1.7.22. - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.7.21...v1.7.22) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index b408ee01eb7..a9aee3b681b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -8,7 +8,7 @@ ext.versions = [ def gradle = "7.3.1" // Ref: https://kotlinlang.org/releases.html -def kotlin = "1.7.21" +def kotlin = "1.7.22" def kotlinCoroutines = "1.6.4" def dagger = "2.44.2" def appDistribution = "16.0.0-beta05" From d6c20226bb97095f55007d66da6076b8d2b4072e Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 8 Dec 2022 13:46:01 +0300 Subject: [PATCH 155/197] Add changelog. --- changelog.d/7740.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7740.feature diff --git a/changelog.d/7740.feature b/changelog.d/7740.feature new file mode 100644 index 00000000000..6cd2b6c7764 --- /dev/null +++ b/changelog.d/7740.feature @@ -0,0 +1 @@ +Handle account data removal From de18f37849fa599eb174ea9f5394867b86378670 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Thu, 8 Dec 2022 11:43:19 +0000 Subject: [PATCH 156/197] [Rich text editor] Add error tracking for rich text editor (#7695) --- changelog.d/7695.bugfix | 1 + .../im/vector/app/core/di/SingletonModule.kt | 4 +++ .../app/features/analytics/VectorAnalytics.kt | 3 +- .../features/analytics/errors/ErrorTracker.kt | 21 +++++++++++++ .../analytics/impl/DefaultVectorAnalytics.kt | 14 ++++++--- .../{SentryFactory.kt => SentryAnalytics.kt} | 9 ++++-- .../composer/MessageComposerFragment.kt | 3 ++ .../detail/composer/RichTextComposerLayout.kt | 10 ++++-- .../composer/RichTextEditorException.kt | 21 +++++++++++++ .../impl/DefaultVectorAnalyticsTest.kt | 31 +++++++++++++++---- ...entryFactory.kt => FakeSentryAnalytics.kt} | 15 +++++++-- 11 files changed, 114 insertions(+), 18 deletions(-) create mode 100644 changelog.d/7695.bugfix create mode 100644 vector/src/main/java/im/vector/app/features/analytics/errors/ErrorTracker.kt rename vector/src/main/java/im/vector/app/features/analytics/impl/{SentryFactory.kt => SentryAnalytics.kt} (88%) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextEditorException.kt rename vector/src/test/java/im/vector/app/test/fakes/{FakeSentryFactory.kt => FakeSentryAnalytics.kt} (74%) diff --git a/changelog.d/7695.bugfix b/changelog.d/7695.bugfix new file mode 100644 index 00000000000..7ec0805bce6 --- /dev/null +++ b/changelog.d/7695.bugfix @@ -0,0 +1 @@ +[Rich text editor] Add error tracking for rich text editor diff --git a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt index 28ca761acef..21a46b0757f 100644 --- a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -46,6 +46,7 @@ import im.vector.app.core.utils.AndroidSystemSettingsProvider import im.vector.app.core.utils.SystemSettingsProvider import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.VectorAnalytics +import im.vector.app.features.analytics.errors.ErrorTracker import im.vector.app.features.analytics.impl.DefaultVectorAnalytics import im.vector.app.features.analytics.metrics.VectorPlugins import im.vector.app.features.invite.AutoAcceptInvites @@ -84,6 +85,9 @@ import javax.inject.Singleton @Binds abstract fun bindVectorAnalytics(analytics: DefaultVectorAnalytics): VectorAnalytics + @Binds + abstract fun bindErrorTracker(analytics: DefaultVectorAnalytics): ErrorTracker + @Binds abstract fun bindAnalyticsTracker(analytics: DefaultVectorAnalytics): AnalyticsTracker diff --git a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt index 7d11f938834..802ba08092b 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt @@ -16,9 +16,10 @@ package im.vector.app.features.analytics +import im.vector.app.features.analytics.errors.ErrorTracker import kotlinx.coroutines.flow.Flow -interface VectorAnalytics : AnalyticsTracker { +interface VectorAnalytics : AnalyticsTracker, ErrorTracker { /** * Return a Flow of Boolean, true if the user has given their consent. */ diff --git a/vector/src/main/java/im/vector/app/features/analytics/errors/ErrorTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/errors/ErrorTracker.kt new file mode 100644 index 00000000000..8ad6bfffc0b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/errors/ErrorTracker.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.analytics.errors + +interface ErrorTracker { + fun trackError(throwable: Throwable) +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt index 553d699d866..ca7608166c0 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -41,7 +41,7 @@ private val IGNORED_OPTIONS: Options? = null @Singleton class DefaultVectorAnalytics @Inject constructor( postHogFactory: PostHogFactory, - private val sentryFactory: SentryFactory, + private val sentryAnalytics: SentryAnalytics, analyticsConfig: AnalyticsConfig, private val analyticsStore: AnalyticsStore, private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory, @@ -97,7 +97,7 @@ class DefaultVectorAnalytics @Inject constructor( setAnalyticsId("") // Close Sentry SDK. - sentryFactory.stopSentry() + sentryAnalytics.stopSentry() } private fun observeAnalyticsId() { @@ -135,8 +135,8 @@ class DefaultVectorAnalytics @Inject constructor( private fun initOrStopSentry() { userConsent?.let { when (it) { - true -> sentryFactory.initSentry() - false -> sentryFactory.stopSentry() + true -> sentryAnalytics.initSentry() + false -> sentryAnalytics.stopSentry() } } } @@ -180,4 +180,10 @@ class DefaultVectorAnalytics @Inject constructor( putAll(this@toPostHogUserProperties.filter { it.value != null }) } } + + override fun trackError(throwable: Throwable) { + sentryAnalytics + .takeIf { userConsent == true } + ?.trackError(throwable) + } } diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/SentryFactory.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/SentryAnalytics.kt similarity index 88% rename from vector/src/main/java/im/vector/app/features/analytics/impl/SentryFactory.kt rename to vector/src/main/java/im/vector/app/features/analytics/impl/SentryAnalytics.kt index a000f2a77ab..21721a31aed 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/SentryFactory.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/SentryAnalytics.kt @@ -18,6 +18,7 @@ package im.vector.app.features.analytics.impl import android.content.Context import im.vector.app.features.analytics.AnalyticsConfig +import im.vector.app.features.analytics.errors.ErrorTracker import im.vector.app.features.analytics.log.analyticsTag import io.sentry.Sentry import io.sentry.SentryOptions @@ -25,10 +26,10 @@ import io.sentry.android.core.SentryAndroid import timber.log.Timber import javax.inject.Inject -class SentryFactory @Inject constructor( +class SentryAnalytics @Inject constructor( private val context: Context, private val analyticsConfig: AnalyticsConfig, -) { +) : ErrorTracker { fun initSentry() { Timber.tag(analyticsTag.value).d("Initializing Sentry") @@ -47,4 +48,8 @@ class SentryFactory @Inject constructor( Timber.tag(analyticsTag.value).d("Stopping Sentry") Sentry.close() } + + override fun trackError(throwable: Throwable) { + Sentry.captureException(throwable) + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index 97e74785ec4..bf9e0ae7260 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -60,6 +60,7 @@ import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentComposerBinding import im.vector.app.features.VectorFeatures +import im.vector.app.features.analytics.errors.ErrorTracker import im.vector.app.features.attachments.AttachmentType import im.vector.app.features.attachments.AttachmentTypeSelectorBottomSheet import im.vector.app.features.attachments.AttachmentTypeSelectorSharedAction @@ -116,6 +117,7 @@ class MessageComposerFragment : VectorBaseFragment(), A @Inject lateinit var vectorFeatures: VectorFeatures @Inject lateinit var buildMeta: BuildMeta @Inject lateinit var session: Session + @Inject lateinit var errorTracker: ErrorTracker private val roomId: String get() = withState(timelineViewModel) { it.roomId } @@ -171,6 +173,7 @@ class MessageComposerFragment : VectorBaseFragment(), A views.composerLayout.isGone = vectorPreferences.isRichTextEditorEnabled() views.richTextComposerLayout.isVisible = vectorPreferences.isRichTextEditorEnabled() + views.richTextComposerLayout.setOnErrorListener(errorTracker::trackError) messageComposerViewModel.observeViewEvents { when (it) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index 48459b5c062..16234c37661 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -49,10 +49,11 @@ import im.vector.app.databinding.ComposerRichTextLayoutBinding import im.vector.app.databinding.ViewRichTextMenuButtonBinding import io.element.android.wysiwyg.EditorEditText import io.element.android.wysiwyg.inputhandlers.models.InlineFormat +import io.element.android.wysiwyg.utils.RustErrorCollector import uniffi.wysiwyg_composer.ActionState import uniffi.wysiwyg_composer.ComposerAction -class RichTextComposerLayout @JvmOverloads constructor( +internal class RichTextComposerLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 @@ -248,10 +249,15 @@ class RichTextComposerLayout @JvmOverloads constructor( updateMenuStateFor(action, state) } } - updateEditTextVisibility() } + fun setOnErrorListener(onError: (e: RichTextEditorException) -> Unit) { + views.richTextComposerEditText.rustErrorCollector = RustErrorCollector { + onError(RichTextEditorException(it)) + } + } + private fun updateEditTextVisibility() { views.richTextComposerEditText.isVisible = isTextFormattingEnabled views.richTextMenu.isVisible = isTextFormattingEnabled diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextEditorException.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextEditorException.kt new file mode 100644 index 00000000000..9bdef59ae3e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextEditorException.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.composer + +internal class RichTextEditorException( + cause: Throwable, +) : Exception(cause) diff --git a/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt index be53f1b9084..3fd0528a19e 100644 --- a/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt +++ b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt @@ -23,7 +23,7 @@ import im.vector.app.test.fakes.FakeAnalyticsStore import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory import im.vector.app.test.fakes.FakePostHog import im.vector.app.test.fakes.FakePostHogFactory -import im.vector.app.test.fakes.FakeSentryFactory +import im.vector.app.test.fakes.FakeSentryAnalytics import im.vector.app.test.fixtures.AnalyticsConfigFixture.anAnalyticsConfig import im.vector.app.test.fixtures.aUserProperties import im.vector.app.test.fixtures.aVectorAnalyticsEvent @@ -46,11 +46,11 @@ class DefaultVectorAnalyticsTest { private val fakePostHog = FakePostHog() private val fakeAnalyticsStore = FakeAnalyticsStore() private val fakeLateInitUserPropertiesFactory = FakeLateInitUserPropertiesFactory() - private val fakeSentryFactory = FakeSentryFactory() + private val fakeSentryAnalytics = FakeSentryAnalytics() private val defaultVectorAnalytics = DefaultVectorAnalytics( postHogFactory = FakePostHogFactory(fakePostHog.instance).instance, - sentryFactory = fakeSentryFactory.instance, + sentryAnalytics = fakeSentryAnalytics.instance, analyticsStore = fakeAnalyticsStore.instance, globalScope = CoroutineScope(Dispatchers.Unconfined), analyticsConfig = anAnalyticsConfig(isEnabled = true), @@ -75,7 +75,7 @@ class DefaultVectorAnalyticsTest { fakePostHog.verifyOptOutStatus(optedOut = false) - fakeSentryFactory.verifySentryInit() + fakeSentryAnalytics.verifySentryInit() } @Test @@ -84,7 +84,7 @@ class DefaultVectorAnalyticsTest { fakePostHog.verifyOptOutStatus(optedOut = true) - fakeSentryFactory.verifySentryClose() + fakeSentryAnalytics.verifySentryClose() } @Test @@ -111,7 +111,7 @@ class DefaultVectorAnalyticsTest { fakePostHog.verifyReset() - fakeSentryFactory.verifySentryClose() + fakeSentryAnalytics.verifySentryClose() } @Test @@ -149,6 +149,25 @@ class DefaultVectorAnalyticsTest { fakePostHog.verifyNoEventTracking() } + + @Test + fun `given user has consented, when tracking exception, then submits to sentry`() = runTest { + fakeAnalyticsStore.givenUserContent(consent = true) + val exception = Exception("test") + + defaultVectorAnalytics.trackError(exception) + + fakeSentryAnalytics.verifySentryTrackError(exception) + } + + @Test + fun `given user has not consented, when tracking exception, then does not track to sentry`() = runTest { + fakeAnalyticsStore.givenUserContent(consent = false) + + defaultVectorAnalytics.trackError(Exception("test")) + + fakeSentryAnalytics.verifyNoErrorTracking() + } } private fun VectorAnalyticsScreen.toPostHogProperties(): Properties? { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSentryFactory.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSentryAnalytics.kt similarity index 74% rename from vector/src/test/java/im/vector/app/test/fakes/FakeSentryFactory.kt rename to vector/src/test/java/im/vector/app/test/fakes/FakeSentryAnalytics.kt index 2628f804359..59f41543b02 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSentryFactory.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSentryAnalytics.kt @@ -16,15 +16,15 @@ package im.vector.app.test.fakes -import im.vector.app.features.analytics.impl.SentryFactory +import im.vector.app.features.analytics.impl.SentryAnalytics import io.mockk.every import io.mockk.mockk import io.mockk.verify -class FakeSentryFactory { +class FakeSentryAnalytics { private var isSentryEnabled = false - val instance = mockk().also { + val instance = mockk(relaxUnitFun = true).also { every { it.initSentry() } answers { isSentryEnabled = true } @@ -41,4 +41,13 @@ class FakeSentryFactory { fun verifySentryClose() { verify { instance.stopSentry() } } + + fun verifySentryTrackError(error: Throwable) { + verify { instance.trackError(error) } + } + + fun verifyNoErrorTracking() = + verify(inverse = true) { + instance.trackError(any()) + } } From df55c841670ff0d13cf911bd80ce161479c1b33a Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 8 Dec 2022 14:00:35 +0100 Subject: [PATCH 157/197] Raise priority of incoming verification request alert + cancel existing verification alerts --- .../IncomingVerificationRequestHandler.kt | 26 ++++++------ .../vector/app/features/home/HomeActivity.kt | 40 ++++++++++++------- .../app/features/home/HomeDetailFragment.kt | 2 +- .../features/home/NewHomeDetailFragment.kt | 2 +- .../app/features/popup/PopupAlertManager.kt | 8 +++- .../vector/app/features/popup/VectorAlert.kt | 2 +- .../features/popup/VerificationVectorAlert.kt | 1 + 7 files changed, 50 insertions(+), 31 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index 3a5c7e7eb88..c749e9578ea 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -72,10 +72,11 @@ class IncomingVerificationRequestHandler @Inject constructor( val user = session.getUserOrDefault(tx.otherUserId).toMatrixItem() val name = user.getBestName() val alert = VerificationVectorAlert( - uid, - context.getString(R.string.sas_incoming_request_notif_title), - context.getString(R.string.sas_incoming_request_notif_content, name), - R.drawable.ic_shield_black, + uid = uid, + title = context.getString(R.string.sas_incoming_request_notif_title), + description = context.getString(R.string.sas_incoming_request_notif_content, name), + iconId = R.drawable.ic_shield_black, + priority = PopupAlertManager.INCOMING_VERIFICATION_REQUEST_PRIORITY, shouldBeDisplayedIn = { activity -> if (activity is VectorBaseActivity<*>) { // TODO a bit too ugly :/ @@ -85,7 +86,7 @@ class IncomingVerificationRequestHandler @Inject constructor( } } ?: true } else true - } + }, ) .apply { viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer.get()) @@ -130,8 +131,8 @@ class IncomingVerificationRequestHandler @Inject constructor( // if not this request will be underneath and not visible by the user... // it will re-appear later if (pr.otherUserId == session?.myUserId) { - // XXX this is a bit hard coded :/ - popupAlertManager.cancelAlert("review_login") + popupAlertManager.cancelAlert(PopupAlertManager.REVIEW_LOGIN_UID) + popupAlertManager.cancelAlert(PopupAlertManager.VERIFY_SESSION_UID) } val user = session.getUserOrDefault(pr.otherUserId).toMatrixItem() val name = user.getBestName() @@ -142,17 +143,18 @@ class IncomingVerificationRequestHandler @Inject constructor( } val alert = VerificationVectorAlert( - uniqueIdForVerificationRequest(pr), - context.getString(R.string.sas_incoming_request_notif_title), - description, - R.drawable.ic_shield_black, + uid = uniqueIdForVerificationRequest(pr), + title = context.getString(R.string.sas_incoming_request_notif_title), + description = description, + iconId = R.drawable.ic_shield_black, + priority = PopupAlertManager.INCOMING_VERIFICATION_REQUEST_PRIORITY, shouldBeDisplayedIn = { activity -> if (activity is RoomDetailActivity) { activity.intent?.extras?.getParcelableCompat(RoomDetailActivity.EXTRA_ROOM_DETAIL_ARGS)?.let { it.roomId != pr.roomId } ?: true } else true - } + }, ) .apply { viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer.get()) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 2a3d8d094c4..e08ed6db465 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -437,9 +437,10 @@ class HomeActivity : private fun handleAskPasswordToInitCrossSigning(events: HomeActivityViewEvents.AskPasswordToInitCrossSigning) { // We need to ask promptSecurityEvent( - events.userItem, - R.string.upgrade_security, - R.string.security_prompt_text + uid = PopupAlertManager.UPGRADE_SECURITY_UID, + userItem = events.userItem, + titleRes = R.string.upgrade_security, + descRes = R.string.security_prompt_text, ) { it.navigator.upgradeSessionSecurity(it, true) } @@ -448,9 +449,10 @@ class HomeActivity : private fun handleCrossSigningInvalidated(event: HomeActivityViewEvents.OnCrossSignedInvalidated) { // We need to ask promptSecurityEvent( - event.userItem, - R.string.crosssigning_verify_this_session, - R.string.confirm_your_identity + uid = PopupAlertManager.VERIFY_SESSION_UID, + userItem = event.userItem, + titleRes = R.string.crosssigning_verify_this_session, + descRes = R.string.confirm_your_identity, ) { it.navigator.waitSessionVerification(it) } @@ -459,9 +461,10 @@ class HomeActivity : private fun handleOnNewSession(event: HomeActivityViewEvents.CurrentSessionNotVerified) { // We need to ask promptSecurityEvent( - event.userItem, - R.string.crosssigning_verify_this_session, - R.string.confirm_your_identity + uid = PopupAlertManager.VERIFY_SESSION_UID, + userItem = event.userItem, + titleRes = R.string.crosssigning_verify_this_session, + descRes = R.string.confirm_your_identity, ) { if (event.waitForIncomingRequest) { it.navigator.waitSessionVerification(it) @@ -474,9 +477,10 @@ class HomeActivity : private fun handleCantVerify(event: HomeActivityViewEvents.CurrentSessionCannotBeVerified) { // We need to ask promptSecurityEvent( - event.userItem, - R.string.crosssigning_cannot_verify_this_session, - R.string.crosssigning_cannot_verify_this_session_desc + uid = PopupAlertManager.UPGRADE_SECURITY_UID, + userItem = event.userItem, + titleRes = R.string.crosssigning_cannot_verify_this_session, + descRes = R.string.crosssigning_cannot_verify_this_session_desc, ) { it.navigator.open4SSetup(it, SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET) } @@ -485,7 +489,7 @@ class HomeActivity : private fun handlePromptToEnablePush() { popupAlertManager.postVectorAlert( DefaultVectorAlert( - uid = "enablePush", + uid = PopupAlertManager.ENABLE_PUSH_UID, title = getString(R.string.alert_push_are_disabled_title), description = getString(R.string.alert_push_are_disabled_description), iconId = R.drawable.ic_room_actions_notifications_mutes, @@ -518,10 +522,16 @@ class HomeActivity : ) } - private fun promptSecurityEvent(userItem: MatrixItem.UserItem, titleRes: Int, descRes: Int, action: ((VectorBaseActivity<*>) -> Unit)) { + private fun promptSecurityEvent( + uid: String, + userItem: MatrixItem.UserItem, + titleRes: Int, + descRes: Int, + action: ((VectorBaseActivity<*>) -> Unit), + ) { popupAlertManager.postVectorAlert( VerificationVectorAlert( - uid = "upgradeSecurity", + uid = uid, title = getString(titleRes), description = getString(descRes), iconId = R.drawable.ic_shield_warning diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 69abeed424f..d310f574dd1 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -156,7 +156,7 @@ class HomeDetailFragment : unknownDeviceDetectorSharedViewModel.onEach { state -> state.unknownSessions.invoke()?.let { unknownDevices -> if (unknownDevices.firstOrNull()?.currentSessionTrust == true) { - val uid = "review_login" + val uid = PopupAlertManager.REVIEW_LOGIN_UID alertManager.cancelAlert(uid) val olderUnverified = unknownDevices.filter { !it.isNew } val newest = unknownDevices.firstOrNull { it.isNew }?.deviceInfo diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt index ccd5a7e84bb..3189c2b99e0 100644 --- a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt @@ -160,7 +160,7 @@ class NewHomeDetailFragment : unknownDeviceDetectorSharedViewModel.onEach { state -> state.unknownSessions.invoke()?.let { unknownDevices -> if (unknownDevices.firstOrNull()?.currentSessionTrust == true) { - val uid = "review_login" + val uid = PopupAlertManager.REVIEW_LOGIN_UID alertManager.cancelAlert(uid) val olderUnverified = unknownDevices.filter { !it.isNew } val newest = unknownDevices.firstOrNull { it.isNew }?.deviceInfo diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index b1327f0caf0..e0310b340ef 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -50,6 +50,12 @@ class PopupAlertManager @Inject constructor( companion object { const val INCOMING_CALL_PRIORITY = Int.MAX_VALUE + const val INCOMING_VERIFICATION_REQUEST_PRIORITY = 1 + const val DEFAULT_PRIORITY = 0 + const val REVIEW_LOGIN_UID = "review_login" + const val UPGRADE_SECURITY_UID = "upgrade_security" + const val VERIFY_SESSION_UID = "verify_session" + const val ENABLE_PUSH_UID = "enable_push" } private var weakCurrentActivity: WeakReference? = null @@ -145,7 +151,7 @@ class PopupAlertManager @Inject constructor( private fun displayNextIfPossible() { val currentActivity = weakCurrentActivity?.get() - if (Alerter.isShowing || currentActivity == null || currentActivity.isDestroyed) { + if (currentActivity == null || currentActivity.isDestroyed) { // will retry later return } diff --git a/vector/src/main/java/im/vector/app/features/popup/VectorAlert.kt b/vector/src/main/java/im/vector/app/features/popup/VectorAlert.kt index ffdba8e04dc..1597d927d8c 100644 --- a/vector/src/main/java/im/vector/app/features/popup/VectorAlert.kt +++ b/vector/src/main/java/im/vector/app/features/popup/VectorAlert.kt @@ -98,7 +98,7 @@ open class DefaultVectorAlert( override val dismissOnClick: Boolean = true - override val priority: Int = 0 + override val priority: Int = PopupAlertManager.DEFAULT_PRIORITY override val isLight: Boolean = false diff --git a/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt b/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt index c2ecbe04b37..818659872f4 100644 --- a/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt +++ b/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt @@ -30,6 +30,7 @@ class VerificationVectorAlert( title: String, override val description: String, @DrawableRes override val iconId: Int?, + override val priority: Int = PopupAlertManager.DEFAULT_PRIORITY, /** * Alert are displayed by default, but let this lambda return false to prevent displaying. */ From 73fd93148ad012dc0f4ab60fd1b892c1a51e21d0 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 2 Dec 2022 18:14:58 +0000 Subject: [PATCH 158/197] Download device keys for self prior to verification checks Fixes https://github.com/vector-im/element-android/issues/7676 --- .../org/matrix/android/sdk/api/rendezvous/Rendezvous.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt index f724ac4b621..d421f8f994d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt @@ -35,7 +35,10 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_S import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.util.MatrixJsonParser +import org.matrix.android.sdk.api.util.awaitCallback import timber.log.Timber /** @@ -147,6 +150,9 @@ class Rendezvous( val deviceKey = crypto.getMyDevice().fingerprint() send(Payload(PayloadType.PROGRESS, outcome = Outcome.SUCCESS, deviceId = deviceId, deviceKey = deviceKey)) + // explicitly download keys for ourself rather than wait for initial sync to complete + awaitCallback> { crypto.downloadKeys(listOf(userId), false, it) } + // await confirmation of verification val verificationResponse = receive() if (verificationResponse?.outcome == Outcome.VERIFIED) { From d0b2c0693de63ff69195c6bb3fac756725e8ac02 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 2 Dec 2022 18:19:02 +0000 Subject: [PATCH 159/197] Changelog --- changelog.d/7699.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7699.bugfix diff --git a/changelog.d/7699.bugfix b/changelog.d/7699.bugfix new file mode 100644 index 00000000000..30a4b8e9fad --- /dev/null +++ b/changelog.d/7699.bugfix @@ -0,0 +1 @@ +Fix E2EE set up failure whilst signing in using QR code From 3a2a916c2f17609fdfba82df08f3160e2685ec68 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 6 Dec 2022 11:34:25 +0000 Subject: [PATCH 160/197] Clarify comment --- .../java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt index d421f8f994d..3364e900c62 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt @@ -150,7 +150,7 @@ class Rendezvous( val deviceKey = crypto.getMyDevice().fingerprint() send(Payload(PayloadType.PROGRESS, outcome = Outcome.SUCCESS, deviceId = deviceId, deviceKey = deviceKey)) - // explicitly download keys for ourself rather than wait for initial sync to complete + // explicitly download keys for ourself rather than racing with initial sync which might not complete in time awaitCallback> { crypto.downloadKeys(listOf(userId), false, it) } // await confirmation of verification From 7bbd91f2a93adcd9ec73aea618912dda17876236 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 7 Dec 2022 15:09:43 +0000 Subject: [PATCH 161/197] Handle error whilst download key for self --- .../org/matrix/android/sdk/api/rendezvous/Rendezvous.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt index 3364e900c62..e5b2d6bf12b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt @@ -150,8 +150,13 @@ class Rendezvous( val deviceKey = crypto.getMyDevice().fingerprint() send(Payload(PayloadType.PROGRESS, outcome = Outcome.SUCCESS, deviceId = deviceId, deviceKey = deviceKey)) - // explicitly download keys for ourself rather than racing with initial sync which might not complete in time - awaitCallback> { crypto.downloadKeys(listOf(userId), false, it) } + try { + // explicitly download keys for ourself rather than racing with initial sync which might not complete in time + awaitCallback> { crypto.downloadKeys(listOf(userId), false, it) } + } catch (e: Throwable) { + // log as warning and continue as initial sync might still complete + Timber.tag(TAG).w(e, "Failed to download keys for self") + } // await confirmation of verification val verificationResponse = receive() From 63bde230a399bc24e6f33e1ab7254024170c7239 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 8 Dec 2022 14:40:17 +0100 Subject: [PATCH 162/197] Cancel verification alerts when adding the incoming request alert and when starting the process --- .../IncomingVerificationRequestHandler.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index c749e9578ea..0f8f5c633e0 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -128,12 +128,9 @@ class IncomingVerificationRequestHandler @Inject constructor( // For incoming request we should prompt (if not in activity where this request apply) if (pr.isIncoming) { // if it's a self verification for my devices, we can discard the review login alert - // if not this request will be underneath and not visible by the user... + // if not, this request will be underneath and not visible by the user... // it will re-appear later - if (pr.otherUserId == session?.myUserId) { - popupAlertManager.cancelAlert(PopupAlertManager.REVIEW_LOGIN_UID) - popupAlertManager.cancelAlert(PopupAlertManager.VERIFY_SESSION_UID) - } + cancelAnyVerifySessionAlerts(pr) val user = session.getUserOrDefault(pr.otherUserId).toMatrixItem() val name = user.getBestName() val description = if (name == pr.otherUserId) { @@ -159,6 +156,7 @@ class IncomingVerificationRequestHandler @Inject constructor( .apply { viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer.get()) contentAction = Runnable { + cancelAnyVerifySessionAlerts(pr) (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { val roomId = pr.roomId if (roomId.isNullOrBlank()) { @@ -188,6 +186,13 @@ class IncomingVerificationRequestHandler @Inject constructor( } } + private fun cancelAnyVerifySessionAlerts(pr: PendingVerificationRequest) { + if (pr.otherUserId == session?.myUserId) { + popupAlertManager.cancelAlert(PopupAlertManager.REVIEW_LOGIN_UID) + popupAlertManager.cancelAlert(PopupAlertManager.VERIFY_SESSION_UID) + } + } + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { // If an incoming request is readied (by another device?) we should discard the alert if (pr.isIncoming && (pr.isReady || pr.handledByOtherSession || pr.cancelConclusion != null)) { From b09a00efdaa6666464babb0ce96e1a8cc52ebaee Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 8 Dec 2022 17:11:09 +0300 Subject: [PATCH 163/197] Code review fixes. --- .../sdk/internal/session/room/RoomAPI.kt | 2 +- .../handler/UserAccountDataSyncHandler.kt | 28 ++++++++++--------- .../parsing/RoomSyncAccountDataHandler.kt | 26 +++++++++-------- .../user/accountdata/AccountDataAPI.kt | 4 +-- 4 files changed, 32 insertions(+), 28 deletions(-) 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 3cf5526a478..4e55b2c40ad 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 @@ -433,7 +433,7 @@ internal interface RoomAPI { * @param roomId the room id * @param type the type */ - @DELETE(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "org.matrix.msc3391/user/{userId}/rooms/{roomId}/account_data/{type}") + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc3391/user/{userId}/rooms/{roomId}/account_data/{type}") suspend fun deleteRoomAccountData( @Path("userId") userId: String, @Path("roomId") roomId: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt index fb2dfa10f6a..6e8b260da87 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt @@ -82,17 +82,13 @@ internal class UserAccountDataSyncHandler @Inject constructor( fun handle(realm: Realm, accountData: UserAccountDataSync?) { accountData?.list?.forEach { event -> - if (event.content.isEmpty()) { - UserAccountDataEntity.delete(realm, event.type) - } else { - // Generic handling, just save in base - handleGenericAccountData(realm, event.type, event.content) - when (event.type) { - UserAccountDataTypes.TYPE_DIRECT_MESSAGES -> handleDirectChatRooms(realm, event) - UserAccountDataTypes.TYPE_PUSH_RULES -> handlePushRules(realm, event) - UserAccountDataTypes.TYPE_IGNORED_USER_LIST -> handleIgnoredUsers(realm, event) - UserAccountDataTypes.TYPE_BREADCRUMBS -> handleBreadcrumbs(realm, event) - } + // Generic handling, just save in base + handleGenericAccountData(realm, event.type, event.content) + when (event.type) { + UserAccountDataTypes.TYPE_DIRECT_MESSAGES -> handleDirectChatRooms(realm, event) + UserAccountDataTypes.TYPE_PUSH_RULES -> handlePushRules(realm, event) + UserAccountDataTypes.TYPE_IGNORED_USER_LIST -> handleIgnoredUsers(realm, event) + UserAccountDataTypes.TYPE_BREADCRUMBS -> handleBreadcrumbs(realm, event) } } } @@ -261,8 +257,14 @@ internal class UserAccountDataSyncHandler @Inject constructor( .equalTo(UserAccountDataEntityFields.TYPE, type) .findFirst() if (existing != null) { - // Update current value - existing.contentStr = ContentMapper.map(content) + if (content.isNullOrEmpty()) { + // This is a response for a deleted account data according to + // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync + UserAccountDataEntity.delete(realm, type) + } else { + // Update current value + existing.contentStr = ContentMapper.map(content) + } } else { realm.createObject(UserAccountDataEntity::class.java).let { accountDataEntity -> accountDataEntity.type = type diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt index c5f82940779..1128e462984 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt @@ -45,17 +45,13 @@ internal class RoomSyncAccountDataHandler @Inject constructor( val roomEntity = RoomEntity.getOrCreate(realm, roomId) for (event in accountData.events) { val eventType = event.getClearType() - if (event.getClearContent().isNullOrEmpty()) { - roomEntity.removeAccountData(eventType) - } else { - handleGeneric(roomEntity, event.getClearContent(), eventType) - if (eventType == RoomAccountDataTypes.EVENT_TYPE_TAG) { - val content = event.getClearContent().toModel() - roomTagHandler.handle(realm, roomId, content) - } else if (eventType == RoomAccountDataTypes.EVENT_TYPE_FULLY_READ) { - val content = event.getClearContent().toModel() - roomFullyReadHandler.handle(realm, roomId, content) - } + handleGeneric(roomEntity, event.getClearContent(), eventType) + if (eventType == RoomAccountDataTypes.EVENT_TYPE_TAG) { + val content = event.getClearContent().toModel() + roomTagHandler.handle(realm, roomId, content) + } else if (eventType == RoomAccountDataTypes.EVENT_TYPE_FULLY_READ) { + val content = event.getClearContent().toModel() + roomFullyReadHandler.handle(realm, roomId, content) } } } @@ -63,7 +59,13 @@ internal class RoomSyncAccountDataHandler @Inject constructor( private fun handleGeneric(roomEntity: RoomEntity, content: JsonDict?, eventType: String) { val existing = roomEntity.accountData.where().equalTo(RoomAccountDataEntityFields.TYPE, eventType).findFirst() if (existing != null) { - existing.contentStr = ContentMapper.map(content) + if (content.isNullOrEmpty()) { + // This is a response for a deleted account data according to + // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync + roomEntity.removeAccountData(eventType) + } else { + existing.contentStr = ContentMapper.map(content) + } } else { val roomAccountData = RoomAccountDataEntity( type = eventType, 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 fd813f1fedd..1b3d59ac665 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 @@ -25,7 +25,7 @@ import retrofit2.http.Path internal interface AccountDataAPI { /** - * Set some account_data for the client. + * Set some account_data for the user. * * @param userId the user id * @param type the type @@ -39,7 +39,7 @@ internal interface AccountDataAPI { ) /** - * Remove an account_data for the client. + * Remove an account_data for the user. * * @param userId the user id * @param type the type From 220b1d86c03bc9c68ad77bdbd9d9cba426fe9366 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 8 Dec 2022 17:41:29 +0100 Subject: [PATCH 164/197] Reverting usage of some stable fields whereas related MSCs have not landed into the specs yet --- .../room/location/StartLiveLocationShareTask.kt | 2 +- .../session/room/location/StopLiveLocationShareTask.kt | 2 +- .../session/room/send/LocalEchoEventFactory.kt | 10 +++++----- .../room/aggregation/poll/PollEventsTestData.kt | 6 +++--- .../DefaultGetActiveBeaconInfoForUserTaskTest.kt | 2 +- .../location/DefaultStartLiveLocationShareTaskTest.kt | 2 +- .../location/DefaultStopLiveLocationShareTaskTest.kt | 2 +- .../LiveLocationShareRedactionEventProcessorTest.kt | 2 +- .../vector/app/test/fakes/FakeCreatePollViewStates.kt | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt index 13753115ac5..dd409fe3a79 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt @@ -46,7 +46,7 @@ internal class DefaultStartLiveLocationShareTask @Inject constructor( isLive = true, unstableTimestampMillis = clock.epochMillis() ).toContent() - val eventType = EventType.STATE_ROOM_BEACON_INFO.stable + val eventType = EventType.STATE_ROOM_BEACON_INFO.unstable val sendStateTaskParams = SendStateTask.Params( roomId = params.roomId, stateKey = userId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt index 40f7aa2dd2a..e1e6fa9d408 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt @@ -45,7 +45,7 @@ internal class DefaultStopLiveLocationShareTask @Inject constructor( val sendStateTaskParams = SendStateTask.Params( roomId = params.roomId, stateKey = stateKey, - eventType = EventType.STATE_ROOM_BEACON_INFO.stable, + eventType = EventType.STATE_ROOM_BEACON_INFO.unstable, body = updatedContent ) return try { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 2f8be694732..8be6b262497 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -181,7 +181,7 @@ internal class LocalEchoEventFactory @Inject constructor( originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.POLL_START.stable, + type = EventType.POLL_START.unstable, content = newContent.toContent().plus(additionalContent.orEmpty()) ) } @@ -206,7 +206,7 @@ internal class LocalEchoEventFactory @Inject constructor( originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.POLL_RESPONSE.stable, + type = EventType.POLL_RESPONSE.unstable, content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) @@ -226,7 +226,7 @@ internal class LocalEchoEventFactory @Inject constructor( originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.POLL_START.stable, + type = EventType.POLL_START.unstable, content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) @@ -249,7 +249,7 @@ internal class LocalEchoEventFactory @Inject constructor( originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.POLL_END.stable, + type = EventType.POLL_END.unstable, content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) @@ -300,7 +300,7 @@ internal class LocalEchoEventFactory @Inject constructor( originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.BEACON_LOCATION_DATA.stable, + type = EventType.BEACON_LOCATION_DATA.unstable, content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) 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 index bdd1fd9b0d2..e38b51132d0 100644 --- 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 @@ -87,7 +87,7 @@ object PollEventsTestData { ) internal val A_POLL_START_EVENT = Event( - type = EventType.POLL_START.stable, + type = EventType.POLL_START.unstable, eventId = AN_EVENT_ID, originServerTs = 1652435922563, senderId = A_USER_ID_1, @@ -96,7 +96,7 @@ object PollEventsTestData { ) internal val A_POLL_RESPONSE_EVENT = Event( - type = EventType.POLL_RESPONSE.stable, + type = EventType.POLL_RESPONSE.unstable, eventId = AN_EVENT_ID, originServerTs = 1652435922563, senderId = A_USER_ID_1, @@ -105,7 +105,7 @@ object PollEventsTestData { ) internal val A_POLL_END_EVENT = Event( - type = EventType.POLL_END.stable, + type = EventType.POLL_END.unstable, eventId = AN_EVENT_ID, originServerTs = 1652435922563, senderId = A_USER_ID_1, diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt index 4a10795647c..6f416a6bc17 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt @@ -69,7 +69,7 @@ class DefaultGetActiveBeaconInfoForUserTaskTest { result shouldBeEqualTo currentStateEvent fakeStateEventDataSource.verifyGetStateEvent( roomId = params.roomId, - eventType = EventType.STATE_ROOM_BEACON_INFO.stable, + eventType = EventType.STATE_ROOM_BEACON_INFO.unstable, stateKey = QueryStringValue.Equals(A_USER_ID) ) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt index a5c126cf72e..3156287774c 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt @@ -75,7 +75,7 @@ internal class DefaultStartLiveLocationShareTaskTest { val expectedParams = SendStateTask.Params( roomId = params.roomId, stateKey = A_USER_ID, - eventType = EventType.STATE_ROOM_BEACON_INFO.stable, + eventType = EventType.STATE_ROOM_BEACON_INFO.unstable, body = expectedBeaconContent ) fakeSendStateTask.verifyExecuteRetry( diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt index a7adadfc63b..03c6f525e0b 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt @@ -79,7 +79,7 @@ class DefaultStopLiveLocationShareTaskTest { val expectedSendParams = SendStateTask.Params( roomId = params.roomId, stateKey = A_USER_ID, - eventType = EventType.STATE_ROOM_BEACON_INFO.stable, + eventType = EventType.STATE_ROOM_BEACON_INFO.unstable, body = expectedBeaconContent ) fakeSendStateTask.verifyExecuteRetry( diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt index d6edb69d93e..8dc7a5c9bc5 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt @@ -79,7 +79,7 @@ class LiveLocationShareRedactionEventProcessorTest { @Test fun `given a redacted live location share event when processing it then related summaries are deleted from database`() = runTest { val event = Event(eventId = AN_EVENT_ID, redacts = A_REDACTED_EVENT_ID) - val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.stable) + val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.unstable) fakeRealm.givenWhere() .givenEqualTo(EventEntityFields.EVENT_ID, A_REDACTED_EVENT_ID) .givenFindFirst(redactedEventEntity) diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt index 42a500671ba..3be1f5c6437 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt @@ -63,7 +63,7 @@ object FakeCreatePollViewStates { ) private val A_POLL_START_EVENT = Event( - type = EventType.POLL_START.stable, + type = EventType.POLL_START.unstable, eventId = A_FAKE_EVENT_ID, originServerTs = 1652435922563, senderId = A_FAKE_USER_ID, From 99942c271451f1586c599c0232b078aa90026747 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 9 Dec 2022 09:33:06 +0100 Subject: [PATCH 165/197] Adding changelog entry --- changelog.d/7751.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7751.bugfix diff --git a/changelog.d/7751.bugfix b/changelog.d/7751.bugfix new file mode 100644 index 00000000000..5d676dbc4d2 --- /dev/null +++ b/changelog.d/7751.bugfix @@ -0,0 +1 @@ +Revert usage of stable fields in live location sharing and polls From cf59c80100d7a4b9ec02f62a43a6350e88fe2e0a Mon Sep 17 00:00:00 2001 From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com> Date: Fri, 9 Dec 2022 09:42:45 +0100 Subject: [PATCH 166/197] stop listening timeline collection changes when app is not resumed (#7734) --- changelog.d/7643.bugfix | 1 + .../vector/app/features/home/room/detail/TimelineFragment.kt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7643.bugfix diff --git a/changelog.d/7643.bugfix b/changelog.d/7643.bugfix new file mode 100644 index 00000000000..66e3f28d5f1 --- /dev/null +++ b/changelog.d/7643.bugfix @@ -0,0 +1 @@ +[Notifications] Fixed a bug when push notification was automatically dismissed while app is on background 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 b73d4438322..6ab20275c25 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 @@ -975,6 +975,7 @@ class TimelineFragment : notificationDrawerManager.setCurrentThread(timelineArgs.threadTimelineArgs?.rootThreadEventId) roomDetailPendingActionStore.data?.let { handlePendingAction(it) } roomDetailPendingActionStore.data = null + views.timelineRecyclerView.adapter = timelineEventController.adapter } private fun handlePendingAction(roomDetailPendingAction: RoomDetailPendingAction) { @@ -993,6 +994,7 @@ class TimelineFragment : super.onPause() notificationDrawerManager.setCurrentRoom(null) notificationDrawerManager.setCurrentThread(null) + views.timelineRecyclerView.adapter = null } private val emojiActivityResultLauncher = registerStartForActivityResult { activityResult -> @@ -1058,7 +1060,6 @@ class TimelineFragment : it.dispatchTo(scrollOnHighlightedEventCallback) } timelineEventController.addModelBuildListener(modelBuildListener) - views.timelineRecyclerView.adapter = timelineEventController.adapter if (vectorPreferences.swipeToReplyIsEnabled()) { val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler { From 57cedaeb6924f7df3939ebdd4c7e74d6af0e66cb Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 9 Dec 2022 10:10:59 +0100 Subject: [PATCH 167/197] Adding changelog entry --- changelog.d/7753.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7753.bugfix diff --git a/changelog.d/7753.bugfix b/changelog.d/7753.bugfix new file mode 100644 index 00000000000..10579b6a845 --- /dev/null +++ b/changelog.d/7753.bugfix @@ -0,0 +1 @@ +[Poll] Poll end event is not recognized From 3d68233723f67d08a6c6bbd819bed5d12d4c5cca Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 9 Dec 2022 14:51:23 +0300 Subject: [PATCH 168/197] Support retrieving account data whose key starts with a string. --- .../accountdata/SessionAccountDataService.kt | 13 +++++++++++++ .../user/accountdata/UserAccountDataDataSource.kt | 10 ++++++++++ 2 files changed, 23 insertions(+) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt index a22dd337745..8addb0782e9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt @@ -63,4 +63,17 @@ interface SessionAccountDataService { * Update the account data with the provided type and the provided account data content. */ suspend fun updateUserAccountData(type: String, content: Content) + + /** + * Retrieve user account data list whose type starts with the given type. + * @param type the type or the starting part of a type + * @return list of account data whose type starts with the given type + */ + fun getUserAccountDataEventsStartWith(type: String): List + + /** + * Deletes user account data of the given type. + * @param type the type to delete from user account data + */ + suspend fun deleteUserAccountData(type: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt index 39f155096ac..2e66f4513be 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt @@ -60,6 +60,16 @@ internal class UserAccountDataDataSource @Inject constructor( ) } + fun getAccountDataEventsStartWith(type: String): List { + return realmSessionProvider.withRealm { realm -> + realm + .where(UserAccountDataEntity::class.java) + .contains(UserAccountDataEntityFields.TYPE, type) + .findAll() + .map(accountDataMapper::map) + } + } + private fun accountDataEventsQuery(realm: Realm, types: Set): RealmQuery { val query = realm.where(UserAccountDataEntity::class.java) if (types.isNotEmpty()) { From 8206b534f9d132a02318436549b05b59bd3853d3 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 9 Dec 2022 14:52:27 +0300 Subject: [PATCH 169/197] Create a task to delete an event data with a given type. --- .../user/accountdata/AccountDataModule.kt | 3 ++ .../DefaultSessionAccountDataService.kt | 11 ++++- .../accountdata/DeleteUserAccountDataTask.kt | 43 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DeleteUserAccountDataTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt index 3173686a275..463292b9c61 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt @@ -42,4 +42,7 @@ internal abstract class AccountDataModule { @Binds abstract fun bindUpdateBreadcrumbsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask + + @Binds + abstract fun bindDeleteUserAccountDataTask(task: DefaultDeleteUserAccountDataTask): DeleteUserAccountDataTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt index c73446cf258..304a586a791 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt @@ -34,10 +34,11 @@ import javax.inject.Inject internal class DefaultSessionAccountDataService @Inject constructor( @SessionDatabase private val monarchy: Monarchy, private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val deleteUserAccountDataTask: DeleteUserAccountDataTask, private val userAccountDataSyncHandler: UserAccountDataSyncHandler, private val userAccountDataDataSource: UserAccountDataDataSource, private val roomAccountDataDataSource: RoomAccountDataDataSource, - private val taskExecutor: TaskExecutor + private val taskExecutor: TaskExecutor, ) : SessionAccountDataService { override fun getUserAccountDataEvent(type: String): UserAccountDataEvent? { @@ -78,4 +79,12 @@ internal class DefaultSessionAccountDataService @Inject constructor( userAccountDataSyncHandler.handleGenericAccountData(realm, type, content) } } + + override fun getUserAccountDataEventsStartWith(type: String): List { + return userAccountDataDataSource.getAccountDataEventsStartWith(type) + } + + override suspend fun deleteUserAccountData(type: String) { + deleteUserAccountDataTask.execute(DeleteUserAccountDataTask.Params(type)) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DeleteUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DeleteUserAccountDataTask.kt new file mode 100644 index 00000000000..8d155e32cbd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DeleteUserAccountDataTask.kt @@ -0,0 +1,43 @@ +/* + * 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.user.accountdata + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface DeleteUserAccountDataTask : Task { + + data class Params( + val type: String, + ) +} + +internal class DefaultDeleteUserAccountDataTask @Inject constructor( + private val accountDataApi: AccountDataAPI, + @UserId private val userId: String, + private val globalErrorReceiver: GlobalErrorReceiver, +) : DeleteUserAccountDataTask { + + override suspend fun execute(params: DeleteUserAccountDataTask.Params) { + return executeRequest(globalErrorReceiver) { + accountDataApi.deleteAccountData(userId, params.type) + } + } +} From 22cce30e353523b4a52085a6201a592fc5a259b2 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 9 Dec 2022 14:53:27 +0300 Subject: [PATCH 170/197] Create use case to detect and delete unnecessary account data of client information. --- .../DeleteUnusedClientInformationUseCase.kt | 39 +++++++++++++++++++ .../settings/devices/v2/DevicesViewModel.kt | 10 +++++ 2 files changed, 49 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt new file mode 100644 index 00000000000..ae77cf74938 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2 + +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.session.clientinfo.MATRIX_CLIENT_INFO_KEY_PREFIX +import javax.inject.Inject + +class DeleteUnusedClientInformationUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + suspend fun execute(deviceFullInfoList: List) { + val expectedClientInfoKeyList = deviceFullInfoList.map { MATRIX_CLIENT_INFO_KEY_PREFIX + it.deviceInfo.deviceId } + activeSessionHolder + .getSafeActiveSession() + ?.accountDataService() + ?.getUserAccountDataEventsStartWith(MATRIX_CLIENT_INFO_KEY_PREFIX) + ?.map { it.type } + ?.subtract(expectedClientInfoKeyList.toSet()) + ?.forEach { userAccountDataKeyToDelete -> + activeSessionHolder.getSafeActiveSession()?.accountDataService()?.deleteUserAccountData(userAccountDataKeyToDelete) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index b7a6c5df30b..d8aeefa3774 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -51,6 +51,7 @@ class DevicesViewModel @AssistedInject constructor( refreshDevicesUseCase: RefreshDevicesUseCase, private val vectorPreferences: VectorPreferences, private val toggleIpAddressVisibilityUseCase: ToggleIpAddressVisibilityUseCase, + private val deleteUnusedClientInformationUseCase: DeleteUnusedClientInformationUseCase, ) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase), @@ -112,6 +113,9 @@ class DevicesViewModel @AssistedInject constructor( val deviceFullInfoList = async.invoke() val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() } val inactiveSessionsCount = deviceFullInfoList.count { it.isInactive } + + deleteUnusedClientInformation(deviceFullInfoList) + copy( devices = async, unverifiedSessionsCount = unverifiedSessionsCount, @@ -125,6 +129,12 @@ class DevicesViewModel @AssistedInject constructor( } } + private fun deleteUnusedClientInformation(deviceFullInfoList: List) { + viewModelScope.launch { + deleteUnusedClientInformationUseCase.execute(deviceFullInfoList) + } + } + private fun refreshDevicesOnCryptoDevicesChange() { viewModelScope.launch { refreshDevicesOnCryptoDevicesChangeUseCase.execute() From 7a667b513e8f21ca1a58ed58fd6f717dfa52bd67 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 9 Dec 2022 15:47:28 +0300 Subject: [PATCH 171/197] Execute use case from a better place. --- .../home/UnknownDeviceDetectorSharedViewModel.kt | 12 ++++++++++++ .../v2/DeleteUnusedClientInformationUseCase.kt | 5 +++-- .../features/settings/devices/v2/DevicesViewModel.kt | 9 --------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt index 21c7bd6ea1d..040ffa7b3fb 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -32,11 +32,13 @@ import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.time.Clock +import im.vector.app.features.settings.devices.v2.DeleteUnusedClientInformationUseCase import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session @@ -66,6 +68,7 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( private val setUnverifiedSessionsAlertShownUseCase: SetUnverifiedSessionsAlertShownUseCase, private val isNewLoginAlertShownUseCase: IsNewLoginAlertShownUseCase, private val setNewLoginAlertShownUseCase: SetNewLoginAlertShownUseCase, + private val deleteUnusedClientInformationUseCase: DeleteUnusedClientInformationUseCase, ) : VectorViewModel(initialState) { sealed class Action : VectorViewModelAction { @@ -102,6 +105,9 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( ) { cryptoList, infoList, pInfo -> // Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}") // Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}") + + deleteUnusedClientInformation(infoList) + infoList .filter { info -> // filter verified session, by checking the crypto device info @@ -143,6 +149,12 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( session.cryptoService().fetchDevicesList(NoOpMatrixCallback()) } + private fun deleteUnusedClientInformation(deviceFullInfoList: List) { + viewModelScope.launch { + deleteUnusedClientInformationUseCase.execute(deviceFullInfoList) + } + } + override fun handle(action: Action) { when (action) { is Action.IgnoreDevice -> { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt index ae77cf74938..9cbca9664ab 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt @@ -18,14 +18,15 @@ package im.vector.app.features.settings.devices.v2 import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.session.clientinfo.MATRIX_CLIENT_INFO_KEY_PREFIX +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import javax.inject.Inject class DeleteUnusedClientInformationUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, ) { - suspend fun execute(deviceFullInfoList: List) { - val expectedClientInfoKeyList = deviceFullInfoList.map { MATRIX_CLIENT_INFO_KEY_PREFIX + it.deviceInfo.deviceId } + suspend fun execute(deviceInfoList: List) { + val expectedClientInfoKeyList = deviceInfoList.map { MATRIX_CLIENT_INFO_KEY_PREFIX + it.deviceId } activeSessionHolder .getSafeActiveSession() ?.accountDataService() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index d8aeefa3774..232fcd50f7f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -51,7 +51,6 @@ class DevicesViewModel @AssistedInject constructor( refreshDevicesUseCase: RefreshDevicesUseCase, private val vectorPreferences: VectorPreferences, private val toggleIpAddressVisibilityUseCase: ToggleIpAddressVisibilityUseCase, - private val deleteUnusedClientInformationUseCase: DeleteUnusedClientInformationUseCase, ) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase), @@ -114,8 +113,6 @@ class DevicesViewModel @AssistedInject constructor( val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() } val inactiveSessionsCount = deviceFullInfoList.count { it.isInactive } - deleteUnusedClientInformation(deviceFullInfoList) - copy( devices = async, unverifiedSessionsCount = unverifiedSessionsCount, @@ -129,12 +126,6 @@ class DevicesViewModel @AssistedInject constructor( } } - private fun deleteUnusedClientInformation(deviceFullInfoList: List) { - viewModelScope.launch { - deleteUnusedClientInformationUseCase.execute(deviceFullInfoList) - } - } - private fun refreshDevicesOnCryptoDevicesChange() { viewModelScope.launch { refreshDevicesOnCryptoDevicesChangeUseCase.execute() From bd91db66f8897955682b4f80eed5aa1b2cee06dc Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 9 Dec 2022 14:07:06 +0100 Subject: [PATCH 172/197] Fixing retrieve of related event id in the end poll event during aggregation --- .../room/aggregation/poll/DefaultPollAggregationProcessor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 90d8e02c393..10c43e3b7f6 100644 --- 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 @@ -156,7 +156,7 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro 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 pollEventId = (event.getRelationContent() ?: content.relatesTo)?.eventId ?: return false val pollOwnerId = getPollEvent(session, roomId, pollEventId)?.root?.senderId val isPollOwner = pollOwnerId == event.senderId From 85a6c8c6f26bdb4bd61e9ca664e3f0be09125fd5 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 9 Dec 2022 19:53:20 +0300 Subject: [PATCH 173/197] Write unit tests for the use case. --- .../DeleteUnusedClientInformationUseCase.kt | 3 + ...eleteUnusedClientInformationUseCaseTest.kt | 137 ++++++++++++++++++ .../fakes/FakeSessionAccountDataService.kt | 4 + 3 files changed, 144 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt index 9cbca9664ab..d98e839b4fe 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt @@ -26,6 +26,9 @@ class DeleteUnusedClientInformationUseCase @Inject constructor( ) { suspend fun execute(deviceInfoList: List) { + // A defensive approach against local storage reports an empty device list (although it is not a seen situation). + if (deviceInfoList.isEmpty()) return + val expectedClientInfoKeyList = deviceInfoList.map { MATRIX_CLIENT_INFO_KEY_PREFIX + it.deviceId } activeSessionHolder .getSafeActiveSession() diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCaseTest.kt new file mode 100644 index 00000000000..68c92042081 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCaseTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2 + +import im.vector.app.core.session.clientinfo.MATRIX_CLIENT_INFO_KEY_PREFIX +import im.vector.app.test.fakes.FakeActiveSessionHolder +import io.mockk.coVerify +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo + +private const val A_CURRENT_DEVICE_ID = "current-device-id" +private const val A_DEVICE_ID_1 = "a-device-id-1" +private const val A_DEVICE_ID_2 = "a-device-id-2" +private const val A_DEVICE_ID_3 = "a-device-id-3" +private const val A_DEVICE_ID_4 = "a-device-id-4" + +private val A_DEVICE_INFO_1 = DeviceInfo(deviceId = A_DEVICE_ID_1) +private val A_DEVICE_INFO_2 = DeviceInfo(deviceId = A_DEVICE_ID_2) + +class DeleteUnusedClientInformationUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val deleteUnusedClientInformationUseCase = DeleteUnusedClientInformationUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + ) + + @Before + fun setup() { + fakeActiveSessionHolder.fakeSession.givenSessionId(A_CURRENT_DEVICE_ID) + } + + @Test + fun `given a device list that account data has all of them and extra devices then use case deletes the unused ones`() = runTest { + // Given + val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2) + val userAccountDataEventList = listOf( + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")), + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")), + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_3, mapOf("key" to "value")), + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_4, mapOf("key" to "value")), + ) + fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith( + type = MATRIX_CLIENT_INFO_KEY_PREFIX, + userAccountDataEventList = userAccountDataEventList, + ) + + // When + deleteUnusedClientInformationUseCase.execute(devices) + + // Then + coVerify { fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_3) } + coVerify { fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_4) } + } + + @Test + fun `given a device list that account data has exactly all of them then use case does nothing`() = runTest { + // Given + val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2) + val userAccountDataEventList = listOf( + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")), + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")), + ) + fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith( + type = MATRIX_CLIENT_INFO_KEY_PREFIX, + userAccountDataEventList = userAccountDataEventList, + ) + + // When + deleteUnusedClientInformationUseCase.execute(devices) + + // Then + coVerify(exactly = 0) { + fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any()) + } + } + + @Test + fun `given a device list that account data has missing some of them then use case does nothing`() = runTest { + // Given + val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2) + val userAccountDataEventList = listOf( + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")), + ) + fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith( + type = MATRIX_CLIENT_INFO_KEY_PREFIX, + userAccountDataEventList = userAccountDataEventList, + ) + + // When + deleteUnusedClientInformationUseCase.execute(devices) + + // Then + coVerify(exactly = 0) { + fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any()) + } + } + + @Test + fun `given an empty device list that account data has some devices then use case does nothing`() = runTest { + // Given + val devices = emptyList() + val userAccountDataEventList = listOf( + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")), + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")), + ) + fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith( + type = MATRIX_CLIENT_INFO_KEY_PREFIX, + userAccountDataEventList = userAccountDataEventList, + ) + + // When + deleteUnusedClientInformationUseCase.execute(devices) + + // Then + coVerify(exactly = 0) { + fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any()) + } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt index f1a0ae74520..cd357ec85a2 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt @@ -54,4 +54,8 @@ class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed fun verifyUpdateUserAccountDataEventSucceeds(type: String, content: Content, inverse: Boolean = false) { coVerify(inverse = inverse) { updateUserAccountData(type, content) } } + + fun givenGetUserAccountDataEventsStartWith(type: String, userAccountDataEventList: List) { + every { getUserAccountDataEventsStartWith(type) } returns userAccountDataEventList + } } From 74d7e60380caf3a625cd47cf67ef150e1664c821 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 09:21:24 +0100 Subject: [PATCH 174/197] Bump fragment from 1.5.4 to 1.5.5 (#7741) Bumps `fragment` from 1.5.4 to 1.5.5. Updates `fragment-ktx` from 1.5.4 to 1.5.5 Updates `fragment-testing` from 1.5.4 to 1.5.5 --- updated-dependencies: - dependency-name: androidx.fragment:fragment-ktx dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: androidx.fragment:fragment-testing dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index a9aee3b681b..dbb5f5fe053 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -27,7 +27,7 @@ def jjwt = "0.11.5" // the whole commit which set version 0.16.0-SNAPSHOT def vanniktechEmoji = "0.16.0-SNAPSHOT" def sentry = "6.9.0" -def fragment = "1.5.4" +def fragment = "1.5.5" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 def espresso = "3.4.0" From 746fb7719a1224926d7b97eb2e5acdc164771379 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 12 Dec 2022 13:39:56 +0300 Subject: [PATCH 175/197] Code review fixes. --- .../sync/handler/UserAccountDataSyncHandler.kt | 18 ++++++++++-------- .../sync/parsing/RoomSyncAccountDataHandler.kt | 15 ++++++++------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt index 6e8b260da87..92ebb41ad9b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt @@ -253,18 +253,20 @@ internal class UserAccountDataSyncHandler @Inject constructor( } fun handleGenericAccountData(realm: Realm, type: String, content: Content?) { + if (content.isNullOrEmpty()) { + // This is a response for a deleted account data according to + // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync + UserAccountDataEntity.delete(realm, type) + return + } + val existing = realm.where() .equalTo(UserAccountDataEntityFields.TYPE, type) .findFirst() + if (existing != null) { - if (content.isNullOrEmpty()) { - // This is a response for a deleted account data according to - // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync - UserAccountDataEntity.delete(realm, type) - } else { - // Update current value - existing.contentStr = ContentMapper.map(content) - } + // Update current value + existing.contentStr = ContentMapper.map(content) } else { realm.createObject(UserAccountDataEntity::class.java).let { accountDataEntity -> accountDataEntity.type = type diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt index 1128e462984..9da12a2c4a8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt @@ -57,15 +57,16 @@ internal class RoomSyncAccountDataHandler @Inject constructor( } private fun handleGeneric(roomEntity: RoomEntity, content: JsonDict?, eventType: String) { + if (content.isNullOrEmpty()) { + // This is a response for a deleted account data according to + // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync + roomEntity.removeAccountData(eventType) + return + } + val existing = roomEntity.accountData.where().equalTo(RoomAccountDataEntityFields.TYPE, eventType).findFirst() if (existing != null) { - if (content.isNullOrEmpty()) { - // This is a response for a deleted account data according to - // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync - roomEntity.removeAccountData(eventType) - } else { - existing.contentStr = ContentMapper.map(content) - } + existing.contentStr = ContentMapper.map(content) } else { val roomAccountData = RoomAccountDataEntity( type = eventType, From a12167077f378bf0aecbe4e1e4b793457b2902b9 Mon Sep 17 00:00:00 2001 From: Ekaterina Gerasimova Date: Fri, 9 Dec 2022 12:19:43 +0000 Subject: [PATCH 176/197] Update project board IDs for automation "PN-" prefixed IDs are no longer working, update to new IDs --- .github/workflows/triage-labelled.yml | 16 ++++++++-------- .../workflows/triage-move-review-requests.yml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index 41cd274b937..036bc069ac1 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -89,7 +89,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc0sUA" + PROJECT_ID: "PVT_kwDOAM0swc0sUA" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} add_product_issues: @@ -113,7 +113,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc4AAg6N" + PROJECT_ID: "PVT_kwDOAM0swc4AAg6N" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} delight_issues_to_board: @@ -139,7 +139,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc1HvQ" + PROJECT_ID: "PVT_kwDOAM0swc1HvQ" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_voice-message_issues: @@ -164,7 +164,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc2KCw" + PROJECT_ID: "PVT_kwDOAM0swc2KCw" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_message_bubbles_issues: name: A-Message-Bubbles to Message bubbles board @@ -188,7 +188,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc3m-g" + PROJECT_ID: "PVT_kwDOAM0swc3m-g" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_ftue_issues: @@ -213,7 +213,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc4AAqVx" + PROJECT_ID: "PVT_kwDOAM0swc4AAqVx" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_WTF_issues: @@ -238,7 +238,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc4AArk0" + PROJECT_ID: "PVT_kwDOAM0swc4AArk0" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_element_x_issues: @@ -268,7 +268,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc4ABTXY" + PROJECT_ID: "PVT_kwDOAM0swc4ABTXY" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ps_features1: diff --git a/.github/workflows/triage-move-review-requests.yml b/.github/workflows/triage-move-review-requests.yml index 6aeba66ccc1..f604b82873d 100644 --- a/.github/workflows/triage-move-review-requests.yml +++ b/.github/workflows/triage-move-review-requests.yml @@ -69,7 +69,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.pull_request.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc0sUA" + PROJECT_ID: "PVT_kwDOAM0swc0sUA" TEAM: "design" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} @@ -138,6 +138,6 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.pull_request.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc4AAg6N" + PROJECT_ID: "PVT_kwDOAM0swc4AAg6N" TEAM: "product" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} From c523e144b816d313e356c2ab1a1a7db4ff99ba30 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 12 Dec 2022 13:52:17 +0100 Subject: [PATCH 177/197] Rich text editor: improve performance when changing composer mode (#7691) * Rich text editor: improve performance when changing composer mode * Add changelog * Make `MessageComposerMode.Quote` and `Reply` data classes * Re-arrange code to fix composer not being emptied when sneding a message --- changelog.d/7691.bugfix | 1 + .../composer/MessageComposerFragment.kt | 2 +- .../detail/composer/MessageComposerMode.kt | 4 +-- .../detail/composer/RichTextComposerLayout.kt | 25 ++++++++++++++----- 4 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 changelog.d/7691.bugfix diff --git a/changelog.d/7691.bugfix b/changelog.d/7691.bugfix new file mode 100644 index 00000000000..02988191434 --- /dev/null +++ b/changelog.d/7691.bugfix @@ -0,0 +1 @@ +Rich Text Editor: improve performance when entering reply/edit/quote mode. diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index bf9e0ae7260..d56ea8b7339 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -285,7 +285,7 @@ class MessageComposerFragment : VectorBaseFragment(), A else -> return } - (composer as? RichTextComposerLayout)?.setFullScreen(setFullScreen) + (composer as? RichTextComposerLayout)?.setFullScreen(setFullScreen, true) messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(setFullScreen)) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt index a401f04bf5c..89cb1486393 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt @@ -23,6 +23,6 @@ sealed interface MessageComposerMode { sealed class Special(open val event: TimelineEvent, open val defaultContent: CharSequence) : MessageComposerMode data class Edit(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent) - class Quote(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent) - class Reply(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent) + data class Quote(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent) + data class Reply(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index 16234c37661..d69fe8edeb9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -66,6 +66,7 @@ internal class RichTextComposerLayout @JvmOverloads constructor( // There is no need to persist these values since they're always updated by the parent fragment private var isFullScreen = false private var hasRelatedMessage = false + private var composerMode: MessageComposerMode? = null var isTextFormattingEnabled = true set(value) { @@ -114,9 +115,15 @@ internal class RichTextComposerLayout @JvmOverloads constructor( private val dimensionConverter = DimensionConverter(resources) - fun setFullScreen(isFullScreen: Boolean) { + fun setFullScreen(isFullScreen: Boolean, animated: Boolean) { + if (!animated && views.composerLayout.layoutParams != null) { + views.composerLayout.updateLayoutParams { + height = + if (isFullScreen) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT + } + } editText.updateLayoutParams { - height = if (isFullScreen) 0 else ViewGroup.LayoutParams.WRAP_CONTENT + height = if (isFullScreen) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT } updateTextFieldBorder(isFullScreen) @@ -371,7 +378,11 @@ internal class RichTextComposerLayout @JvmOverloads constructor( override fun renderComposerMode(mode: MessageComposerMode) { if (mode is MessageComposerMode.Special) { views.composerModeGroup.isVisible = true - replaceFormattedContent(mode.defaultContent) + if (isTextFormattingEnabled) { + replaceFormattedContent(mode.defaultContent) + } else { + views.plainTextComposerEditText.setText(mode.defaultContent) + } hasRelatedMessage = true editText.showKeyboard(andRequestFocus = true) } else { @@ -383,10 +394,14 @@ internal class RichTextComposerLayout @JvmOverloads constructor( views.plainTextComposerEditText.setText(text) } } - views.sendButton.contentDescription = resources.getString(R.string.action_send) hasRelatedMessage = false } + updateTextFieldBorder(isFullScreen) + + if (this.composerMode == mode) return + this.composerMode = mode + views.sendButton.apply { if (mode is MessageComposerMode.Edit) { contentDescription = resources.getString(R.string.action_save) @@ -397,8 +412,6 @@ internal class RichTextComposerLayout @JvmOverloads constructor( } } - updateTextFieldBorder(isFullScreen) - when (mode) { is MessageComposerMode.Edit -> { views.composerModeTitleView.setText(R.string.editing) From 8c6c2dd5c2f5ed1777f8da128c82adddf268a589 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 12 Dec 2022 16:36:40 +0300 Subject: [PATCH 178/197] Code review fixes. --- changelog.d/7754.feature | 1 + .../accountdata/UserAccountDataDataSource.kt | 2 +- .../DefaultDeleteUserAccountDataTaskTest.kt | 53 +++++++++++++++++++ .../sdk/test/fakes/FakeAccountDataApi.kt | 32 +++++++++++ .../DeleteUnusedClientInformationUseCase.kt | 7 ++- .../UnknownDeviceDetectorSharedViewModel.kt | 2 +- ...eleteUnusedClientInformationUseCaseTest.kt | 3 +- 7 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 changelog.d/7754.feature create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultDeleteUserAccountDataTaskTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAccountDataApi.kt rename vector/src/main/java/im/vector/app/{features/settings/devices/v2 => core/session/clientinfo}/DeleteUnusedClientInformationUseCase.kt (87%) rename vector/src/test/java/im/vector/app/{features/settings/devices/v2 => core/session/clientinfo}/DeleteUnusedClientInformationUseCaseTest.kt (97%) diff --git a/changelog.d/7754.feature b/changelog.d/7754.feature new file mode 100644 index 00000000000..0e1b6d09617 --- /dev/null +++ b/changelog.d/7754.feature @@ -0,0 +1 @@ +Delete unused client information from account data diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt index 2e66f4513be..01f5d9f708a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt @@ -64,7 +64,7 @@ internal class UserAccountDataDataSource @Inject constructor( return realmSessionProvider.withRealm { realm -> realm .where(UserAccountDataEntity::class.java) - .contains(UserAccountDataEntityFields.TYPE, type) + .beginsWith(UserAccountDataEntityFields.TYPE, type) .findAll() .map(accountDataMapper::map) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultDeleteUserAccountDataTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultDeleteUserAccountDataTaskTest.kt new file mode 100644 index 00000000000..86580127dc2 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultDeleteUserAccountDataTaskTest.kt @@ -0,0 +1,53 @@ +/* + * 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.user.accountdata + +import io.mockk.coVerify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.test.fakes.FakeAccountDataApi +import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver + +private const val A_TYPE = "a-type" +private const val A_USER_ID = "a-user-id" + +@ExperimentalCoroutinesApi +class DefaultDeleteUserAccountDataTaskTest { + + private val fakeGlobalErrorReceiver = FakeGlobalErrorReceiver() + private val fakeAccountDataApi = FakeAccountDataApi() + + private val deleteUserAccountDataTask = DefaultDeleteUserAccountDataTask( + accountDataApi = fakeAccountDataApi.instance, + userId = A_USER_ID, + globalErrorReceiver = fakeGlobalErrorReceiver + ) + + @Test + fun `given parameters when executing the task then api is called`() = runTest { + // Given + val params = DeleteUserAccountDataTask.Params(type = A_TYPE) + fakeAccountDataApi.givenParamsToDeleteAccountData(A_USER_ID, A_TYPE) + + // When + deleteUserAccountDataTask.execute(params) + + // Then + coVerify { fakeAccountDataApi.instance.deleteAccountData(A_USER_ID, A_TYPE) } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAccountDataApi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAccountDataApi.kt new file mode 100644 index 00000000000..f3acc02458f --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAccountDataApi.kt @@ -0,0 +1,32 @@ +/* + * 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.test.fakes + +import io.mockk.coEvery +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataAPI + +internal class FakeAccountDataApi { + + val instance: AccountDataAPI = mockk() + + fun givenParamsToDeleteAccountData(userId: String, type: String) { + coEvery { instance.deleteAccountData(userId, type) } just runs + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt b/vector/src/main/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCase.kt similarity index 87% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt rename to vector/src/main/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCase.kt index d98e839b4fe..dcd5c584800 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCase.kt @@ -14,10 +14,9 @@ * limitations under the License. */ -package im.vector.app.features.settings.devices.v2 +package im.vector.app.core.session.clientinfo import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.session.clientinfo.MATRIX_CLIENT_INFO_KEY_PREFIX import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import javax.inject.Inject @@ -25,9 +24,9 @@ class DeleteUnusedClientInformationUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, ) { - suspend fun execute(deviceInfoList: List) { + suspend fun execute(deviceInfoList: List): Result = runCatching { // A defensive approach against local storage reports an empty device list (although it is not a seen situation). - if (deviceInfoList.isEmpty()) return + if (deviceInfoList.isEmpty()) return Result.success(Unit) val expectedClientInfoKeyList = deviceInfoList.map { MATRIX_CLIENT_INFO_KEY_PREFIX + it.deviceId } activeSessionHolder diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt index 040ffa7b3fb..de9e496ee17 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -31,8 +31,8 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.core.session.clientinfo.DeleteUnusedClientInformationUseCase import im.vector.app.core.time.Clock -import im.vector.app.features.settings.devices.v2.DeleteUnusedClientInformationUseCase import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCaseTest.kt similarity index 97% rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCaseTest.kt rename to vector/src/test/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCaseTest.kt index 68c92042081..8acb7b404b5 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCaseTest.kt @@ -14,9 +14,8 @@ * limitations under the License. */ -package im.vector.app.features.settings.devices.v2 +package im.vector.app.core.session.clientinfo -import im.vector.app.core.session.clientinfo.MATRIX_CLIENT_INFO_KEY_PREFIX import im.vector.app.test.fakes.FakeActiveSessionHolder import io.mockk.coVerify import kotlinx.coroutines.test.runTest From 1930047ce18c1aafa6ea334713d6e733987cac9d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 7 Dec 2022 15:15:18 +0000 Subject: [PATCH 179/197] Fix issue of QR not being offered where domain is entered instead of homeserver --- .../sdk/api/auth/AuthenticationService.kt | 6 ------ .../sdk/api/auth/data/LoginFlowResult.kt | 3 ++- .../auth/DefaultAuthenticationService.kt | 17 ++--------------- .../features/onboarding/OnboardingViewModel.kt | 11 ++++++----- .../features/onboarding/OnboardingViewState.kt | 1 + .../StartAuthenticationFlowUseCase.kt | 3 ++- 6 files changed, 13 insertions(+), 28 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt index 252c33a8c4c..e490311b916 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -125,12 +125,6 @@ interface AuthenticationService { deviceId: String? = null ): Session - /** - * @param homeServerConnectionConfig the information about the homeserver and other configuration - * Return true if qr code login is supported by the server, false otherwise. - */ - suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean - /** * Authenticate using m.login.token method during sign in with QR code. * @param homeServerConnectionConfig the information about the homeserver and other configuration diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt index 5b6c1897bf7..5de83033e1c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt @@ -22,5 +22,6 @@ data class LoginFlowResult( val isLoginAndRegistrationSupported: Boolean, val homeServerUrl: String, val isOutdatedHomeserver: Boolean, - val isLogoutDevicesSupported: Boolean + val isLogoutDevicesSupported: Boolean, + val isLoginWithQrSupported: Boolean, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 5449c0a735f..6556c3a9b3d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -299,7 +299,8 @@ internal class DefaultAuthenticationService @Inject constructor( isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl = homeServerUrl, isOutdatedHomeserver = !versions.isSupportedBySdk(), - isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices() + isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(), + isLoginWithQrSupported = versions.doesServerSupportQrCodeLogin(), ) } @@ -408,20 +409,6 @@ internal class DefaultAuthenticationService @Inject constructor( ) } - override suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean { - val authAPI = buildAuthAPI(homeServerConnectionConfig) - val versions = runCatching { - executeRequest(null) { - authAPI.versions() - } - } - return if (versions.isSuccess) { - versions.getOrNull()?.doesServerSupportQrCodeLogin().orFalse() - } else { - false - } - } - override suspend fun loginUsingQrLoginToken( homeServerConnectionConfig: HomeServerConnectionConfig, loginToken: String, 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 7fe73f8087f..b0964556118 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 @@ -118,7 +118,7 @@ class OnboardingViewModel @AssistedInject constructor( } } - private suspend fun checkQrCodeLoginCapability(config: HomeServerConnectionConfig) { + private fun checkQrCodeLoginCapability() { if (!vectorFeatures.isQrCodeLoginEnabled()) { setState { copy( @@ -133,11 +133,9 @@ class OnboardingViewModel @AssistedInject constructor( ) } } else { - // check if selected server supports MSC3882 first - val canLoginWithQrCode = authenticationService.isQrLoginSupported(config) setState { copy( - canLoginWithQrCode = canLoginWithQrCode + canLoginWithQrCode = selectedHomeserver.isLoginWithQrSupported ) } } @@ -705,7 +703,10 @@ class OnboardingViewModel @AssistedInject constructor( // This is invalid _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) } else { - startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction) + startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, suspend { + checkQrCodeLoginCapability() + postAction() + }) } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt index 6e7d58338e0..ea0d940952f 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt @@ -76,6 +76,7 @@ data class SelectedHomeserverState( val preferredLoginMode: LoginMode = LoginMode.Unknown, val supportedLoginTypes: List = emptyList(), val isLogoutDevicesSupported: Boolean = false, + val isLoginWithQrSupported: Boolean = false, ) : Parcelable @Parcelize diff --git a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt index db21a538541..9b8f0a1cc44 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt @@ -47,7 +47,8 @@ class StartAuthenticationFlowUseCase @Inject constructor( upstreamUrl = authFlow.homeServerUrl, preferredLoginMode = preferredLoginMode, supportedLoginTypes = authFlow.supportedLoginTypes, - isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported + isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported, + isLoginWithQrSupported = authFlow.isLoginWithQrSupported, ) private fun LoginFlowResult.findPreferredLoginMode() = when { From 21cbe527400b627739e87b6efbfcf73826b6bf9d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 7 Dec 2022 15:38:25 +0000 Subject: [PATCH 180/197] Lint --- .../android/sdk/internal/auth/DefaultAuthenticationService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 6556c3a9b3d..d9c2afcb408 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -30,7 +30,6 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.wellknown.WellknownResult -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixIdFailure import org.matrix.android.sdk.api.session.Session From 006e2b5c0d1c0a9cc418a6657eab410f3ed8b16d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 7 Dec 2022 15:46:47 +0000 Subject: [PATCH 181/197] Changelog --- changelog.d/7737.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7737.bugfix diff --git a/changelog.d/7737.bugfix b/changelog.d/7737.bugfix new file mode 100644 index 00000000000..14778346743 --- /dev/null +++ b/changelog.d/7737.bugfix @@ -0,0 +1 @@ +Fix issue of Scan QR code button sometimes not showing when it should be available From 1437f6d41d0ad75e58b5f31b26c6815ba1b0f655 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 9 Dec 2022 16:59:49 +0000 Subject: [PATCH 182/197] Remove unused bad function call --- .../im/vector/app/features/onboarding/OnboardingViewModel.kt | 2 -- 1 file changed, 2 deletions(-) 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 b0964556118..04487c6198f 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 @@ -765,8 +765,6 @@ class OnboardingViewModel @AssistedInject constructor( _viewEvents.post(OnboardingViewEvents.OutdatedHomeserver) } - checkQrCodeLoginCapability(config) - when (trigger) { is OnboardingAction.HomeServerChange.SelectHomeServer -> { onHomeServerSelected(config, serverTypeOverride, authResult) From 643b09a77c9ae9c0ef5b2bdbd4abf16e12414ad1 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 12 Dec 2022 11:12:44 +0000 Subject: [PATCH 183/197] Fix up unit tests --- .../features/onboarding/StartAuthenticationFlowUseCaseTest.kt | 3 ++- .../im/vector/app/test/fakes/FakeAuthenticationService.kt | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt index d15a6cf0421..9be22d7ea99 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt @@ -140,7 +140,8 @@ class StartAuthenticationFlowUseCaseTest { isLoginAndRegistrationSupported = true, homeServerUrl = A_DECLARED_HOMESERVER_URL, isOutdatedHomeserver = false, - isLogoutDevicesSupported = false + isLogoutDevicesSupported = false, + isLoginWithQrSupported = false ) private fun expectedResult( diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt index 5d0e317c570..af539131693 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt @@ -58,10 +58,6 @@ class FakeAuthenticationService : AuthenticationService by mockk() { coEvery { getWellKnownData(matrixId, config) } returns result } - fun givenIsQrLoginSupported(config: HomeServerConnectionConfig, result: Boolean) { - coEvery { isQrLoginSupported(config) } returns result - } - fun givenWellKnownThrows(matrixId: String, config: HomeServerConnectionConfig?, cause: Throwable) { coEvery { getWellKnownData(matrixId, config) } throws cause } From 096e52612e94ce672e7f972f0dceca4eab05be6d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 12 Dec 2022 11:18:20 +0000 Subject: [PATCH 184/197] More fix up of unit tests --- .../vector/app/features/onboarding/OnboardingViewModelTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index 92083eb50b6..f41a27ba7d6 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -1180,7 +1180,6 @@ class OnboardingViewModelTest { fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration) fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString()) - fakeAuthenticationService.givenIsQrLoginSupported(config, canLoginWithQrCode) } private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) { @@ -1188,7 +1187,6 @@ class OnboardingViewModelTest { fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error)) fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString()) - fakeAuthenticationService.givenIsQrLoginSupported(A_HOMESERVER_CONFIG, false) } private fun givenUserNameIsAvailable(userName: String) { From f111a84e1745125024c4b7281ec3160874d0aef4 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 12 Dec 2022 14:07:01 +0000 Subject: [PATCH 185/197] More unit test fix --- .../vector/app/features/onboarding/OnboardingViewModelTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index f41a27ba7d6..2649b82f725 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -164,7 +164,7 @@ class OnboardingViewModelTest { fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver qrCode supported`() = runTest { val test = viewModel.test() fakeVectorFeatures.givenCombinedLoginEnabled() - givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE, canLoginWithQrCode = true) + givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE) viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn)) @@ -1174,7 +1174,6 @@ class OnboardingViewModelTest { resultingState: SelectedHomeserverState, config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG, fingerprint: Fingerprint? = null, - canLoginWithQrCode: Boolean = false, ) { fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config) fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) From 0ffc2af679e9a1980c74cad83489d5a7d08cbcff Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 12 Dec 2022 17:32:28 +0000 Subject: [PATCH 186/197] Update test to work with new state --- .../app/features/onboarding/OnboardingViewModelTest.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index 2649b82f725..c570a75d99e 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -83,6 +83,7 @@ private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance) private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password, userFacingUrl = A_HOMESERVER_URL) private val SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES = SelectedHomeserverState(isLogoutDevicesSupported = true) private val DEFAULT_SELECTED_HOMESERVER_STATE = SELECTED_HOMESERVER_STATE.copy(userFacingUrl = A_DEFAULT_HOMESERVER_URL) +private val DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED = DEFAULT_SELECTED_HOMESERVER_STATE.copy(isLoginWithQrSupported = true) private const val AN_EMAIL = "hello@example.com" private const val A_PASSWORD = "a-password" private const val A_USERNAME = "hello-world" @@ -164,7 +165,7 @@ class OnboardingViewModelTest { fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver qrCode supported`() = runTest { val test = viewModel.test() fakeVectorFeatures.givenCombinedLoginEnabled() - givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE) + givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED) viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn)) @@ -173,9 +174,9 @@ class OnboardingViewModelTest { initialState, { copy(onboardingFlow = OnboardingFlow.SignIn) }, { copy(isLoading = true) }, - { copy(canLoginWithQrCode = true) }, - { copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE) }, + { copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED) }, { copy(signMode = SignMode.SignIn) }, + { copy(canLoginWithQrCode = true) }, { copy(isLoading = false) } ) .assertEvents(OnboardingViewEvents.OpenCombinedLogin) From 4e0c3a97bdd45603ede10b11bd928c79f771bf58 Mon Sep 17 00:00:00 2001 From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com> Date: Mon, 12 Dec 2022 22:35:09 +0100 Subject: [PATCH 187/197] thread message notification should navigate to thread timeline (#7771) --- changelog.d/7770.bugfix | 1 + .../home/room/threads/ThreadsActivity.kt | 12 ++++- .../notifications/NotificationUtils.kt | 52 +++++++++++++++++-- .../notifications/RoomGroupMessageCreator.kt | 9 ++-- 4 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 changelog.d/7770.bugfix diff --git a/changelog.d/7770.bugfix b/changelog.d/7770.bugfix new file mode 100644 index 00000000000..598deb60735 --- /dev/null +++ b/changelog.d/7770.bugfix @@ -0,0 +1 @@ +[Push Notifications] When push notification for threaded message is clicked, thread timeline will be opened instead of room's main timeline diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt index b3f2ef1f755..014b9f0504e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt @@ -26,6 +26,7 @@ import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityThreadsBinding +import im.vector.app.features.MainActivity import im.vector.app.features.analytics.extensions.toAnalyticsInteraction import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.home.AvatarRenderer @@ -143,13 +144,20 @@ class ThreadsActivity : VectorBaseActivity() { context: Context, threadTimelineArgs: ThreadTimelineArgs?, threadListArgs: ThreadListArgs?, - eventIdToNavigate: String? = null + eventIdToNavigate: String? = null, + firstStartMainActivity: Boolean = false ): Intent { - return Intent(context, ThreadsActivity::class.java).apply { + val intent = Intent(context, ThreadsActivity::class.java).apply { putExtra(THREAD_TIMELINE_ARGS, threadTimelineArgs) putExtra(THREAD_EVENT_ID_TO_NAVIGATE, eventIdToNavigate) putExtra(THREAD_LIST_ARGS, threadListArgs) } + + return if (firstStartMainActivity) { + MainActivity.getIntentWithNextIntent(context, intent) + } else { + intent + } } } 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 bf1b23093db..7bf78bdb950 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 @@ -60,6 +60,8 @@ import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.arguments.TimelineArgs +import im.vector.app.features.home.room.threads.ThreadsActivity +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.troubleshoot.TestNotificationReceiver import im.vector.app.features.themes.ThemeUtils @@ -574,6 +576,7 @@ class NotificationUtils @Inject constructor( fun buildMessagesListNotification( messageStyle: NotificationCompat.MessagingStyle, roomInfo: RoomEventGroupInfo, + threadId: String?, largeIcon: Bitmap?, lastMessageTimestamp: Long, senderDisplayNameForReplyCompat: String?, @@ -581,7 +584,11 @@ class NotificationUtils @Inject constructor( ): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) // Build the pending intent for when the notification is clicked - val openRoomIntent = buildOpenRoomIntent(roomInfo.roomId) + val openIntent = when { + threadId != null && vectorPreferences.areThreadMessagesEnabled() -> buildOpenThreadIntent(roomInfo, threadId) + else -> buildOpenRoomIntent(roomInfo.roomId) + } + val smallIcon = R.drawable.ic_notification val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID @@ -666,8 +673,8 @@ class NotificationUtils @Inject constructor( } } - if (openRoomIntent != null) { - setContentIntent(openRoomIntent) + if (openIntent != null) { + setContentIntent(openIntent) } if (largeIcon != null) { @@ -826,6 +833,45 @@ class NotificationUtils @Inject constructor( ) } + private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: String?): PendingIntent? { + val threadTimelineArgs = ThreadTimelineArgs( + startsThread = false, + roomId = roomInfo.roomId, + rootThreadEventId = threadId, + showKeyboard = false, + displayName = roomInfo.roomDisplayName, + avatarUrl = null, + roomEncryptionTrustLevel = null, + ) + val threadIntentTap = ThreadsActivity.newIntent( + context = context, + threadTimelineArgs = threadTimelineArgs, + threadListArgs = null, + firstStartMainActivity = true, + ) + threadIntentTap.action = actionIds.tapToView + // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that + threadIntentTap.data = createIgnoredUri("openThread?$threadId") + + val roomIntent = RoomDetailActivity.newIntent( + context = context, + timelineArgs = TimelineArgs( + roomId = roomInfo.roomId, + switchToParentSpace = true + ), + firstStartMainActivity = false + ) + // Recreate the back stack + return TaskStackBuilder.create(context) + .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false)) + .addNextIntentWithParentStack(roomIntent) + .addNextIntent(threadIntentTap) + .getPendingIntent( + clock.epochMillis().toInt(), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + } + private fun buildOpenHomePendingIntentForSummary(): PendingIntent { val intent = HomeActivity.newIntent(context, firstStartMainActivity = false, clearNotification = true) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP diff --git a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt index 7fdfa3535ea..767f427f392 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt @@ -33,14 +33,14 @@ class RoomGroupMessageCreator @Inject constructor( ) { fun createRoomMessage(events: List, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message { - val firstKnownRoomEvent = events[0] - val roomName = firstKnownRoomEvent.roomName ?: firstKnownRoomEvent.senderName ?: "" - val roomIsGroup = !firstKnownRoomEvent.roomIsDirect + val lastKnownRoomEvent = events.last() + val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "" + val roomIsGroup = !lastKnownRoomEvent.roomIsDirect val style = NotificationCompat.MessagingStyle( Person.Builder() .setName(userDisplayName) .setIcon(bitmapLoader.getUserIcon(userAvatarUrl)) - .setKey(firstKnownRoomEvent.matrixID) + .setKey(lastKnownRoomEvent.matrixID) .build() ).also { it.conversationTitle = roomName.takeIf { roomIsGroup } @@ -75,6 +75,7 @@ class RoomGroupMessageCreator @Inject constructor( it.customSound = events.last().soundName it.isUpdated = events.last().isUpdated }, + threadId = lastKnownRoomEvent.threadId, largeIcon = largeBitmap, lastMessageTimestamp, userDisplayName, From d05e10e10a952bfc1cdee8cdeab49585caa46ca0 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 13 Dec 2022 11:38:49 +0100 Subject: [PATCH 188/197] crypto migration tests (#7645) Crypto migration tests Co-authored-by: Benoit Marty --- .gitattributes | 1 + changelog.d/7645.misc | 1 + docs/database_migration_test.md | 55 +++++++++++++++ .../androidTest/assets/crypto_store_20.realm | 3 + .../src/androidTest/assets/session_42.realm | Bin 270336 -> 131 bytes .../database/CryptoSanityMigrationTest.kt | 65 ++++++++++++++++++ 6 files changed, 125 insertions(+) create mode 100644 changelog.d/7645.misc create mode 100644 docs/database_migration_test.md create mode 100644 matrix-sdk-android/src/androidTest/assets/crypto_store_20.realm create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt diff --git a/.gitattributes b/.gitattributes index 0542767effc..b44f3fab1b4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ **/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text +**/src/androidTest/assets/*.realm filter=lfs diff=lfs merge=lfs -text diff --git a/changelog.d/7645.misc b/changelog.d/7645.misc new file mode 100644 index 00000000000..a133581ac12 --- /dev/null +++ b/changelog.d/7645.misc @@ -0,0 +1 @@ +Crypto database migration tests diff --git a/docs/database_migration_test.md b/docs/database_migration_test.md new file mode 100644 index 00000000000..f7844abde8a --- /dev/null +++ b/docs/database_migration_test.md @@ -0,0 +1,55 @@ + + +* [Testing database migration](#testing-database-migration) + * [Creating a reference database](#creating-a-reference-database) + * [Testing](#testing) + + + +## Testing database migration + +### Creating a reference database + +Databases are encrypted, the key to decrypt is needed to setup the test. +A special build property must be enabled to extract it. + +Set `vector.debugPrivateData=true` in `~/.gradle/gradle.properties` (to avoid committing by mistake) + +Launch the app in your emulator, login and use the app to fill up the database. + +Save the key for the tested database +``` +RealmKeysUtils W Database key for alias `session_db_fe9f212a611ccf6dea1141777065ed0a`: 935a6dfa0b0fc5cce1414194ed190.... +RealmKeysUtils W Database key for alias `crypto_module_fe9f212a611ccf6dea1141777065ed0a`: 7b9a21a8a311e85d75b069a343..... +``` + + +Use the [Device File Explorer](https://developer.android.com/studio/debug/device-file-explorer) to extrat the database file from the emulator. + +Go to `data/data/im.vector.app.debug/files//` +Pick the database you want to test (name can be found in SessionRealmConfigurationFactory): + - crypto_store.realm for crypto + - disk_store.realm for session + - etc... + +Download the file on your disk + +### Testing + +Copy the file in `src/AndroidTest/assets` + +see `CryptoSanityMigrationTest` or `RealmSessionStoreMigration43Test` for sample tests. + +There are already some databases in the assets folder. +The existing test will properly detect schema changes, and fail with such errors if a migration is missing: + +``` +io.realm.exceptions.RealmMigrationNeededException: Migration is required due to the following errors: +- Property 'CryptoMetadataEntity.foo' has been added. +``` + +If you want to test properly more complex database migration (dynamic transforms) ensure that the database contains +the entity you want to migrate. + +You can explore the database with [realm studio](https://www.mongodb.com/docs/realm/studio/) if needed. + diff --git a/matrix-sdk-android/src/androidTest/assets/crypto_store_20.realm b/matrix-sdk-android/src/androidTest/assets/crypto_store_20.realm new file mode 100644 index 00000000000..cfdd2e6da6d --- /dev/null +++ b/matrix-sdk-android/src/androidTest/assets/crypto_store_20.realm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7acd69f37612bab0a1ab7f456656712d7ba19dbb679f81b97b58ef44e239f42 +size 8523776 diff --git a/matrix-sdk-android/src/androidTest/assets/session_42.realm b/matrix-sdk-android/src/androidTest/assets/session_42.realm index b92d13dab2fa6d44a38ee3443ae8cad85340db23..92681116994c0fc88108bb9b54d22b86f5b260bb 100644 GIT binary patch literal 131 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

~n*FN$6ddX2RG}!itYe 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%0DTX_=9PRTAUr6@kN4E#1dFf1 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 L00KbZ|0M7~<4-Vn diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt new file mode 100644 index 00000000000..2643bf643a0 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database + +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import io.realm.Realm +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule +import org.matrix.android.sdk.internal.util.time.Clock + +class CryptoSanityMigrationTest { + @get:Rule val configurationFactory = TestRealmConfigurationFactory() + + lateinit var context: Context + var realm: Realm? = null + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().context + } + + @After + fun tearDown() { + realm?.close() + } + + @Test + fun cryptoDatabaseShouldMigrateGracefully() { + val realmName = "crypto_store_20.realm" + val migration = RealmCryptoStoreMigration(object : Clock { + override fun epochMillis(): Long { + return 0L + } + }) + val realmConfiguration = configurationFactory.createConfiguration( + realmName, + "7b9a21a8a311e85d75b069a343c23fc952fc3fec5e0c83ecfa13f24b787479c487c3ed587db3dd1f5805d52041fc0ac246516e94b27ffa699ff928622e621aca", + RealmCryptoStoreModule(), + migration.schemaVersion, + migration + ) + configurationFactory.copyRealmFromAssets(context, realmName, realmName) + + realm = Realm.getInstance(realmConfiguration) + } +} From 4657729e3619c61d7767e0bbd78f6a12e2926410 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Dec 2022 12:41:17 +0000 Subject: [PATCH 189/197] Bump dependency-check-gradle from 7.3.0 to 7.4.1 (#7759) Bumps dependency-check-gradle from 7.3.0 to 7.4.1. --- updated-dependencies: - dependency-name: org.owasp:dependency-check-gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2abb2a9072c..0f94fc418cf 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ buildscript { classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' classpath "com.likethesalad.android:stem-plugin:2.2.3" - classpath 'org.owasp:dependency-check-gradle:7.3.0' + classpath 'org.owasp:dependency-check-gradle:7.4.1' classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0" classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3' From 3dadebe505d52a86ab1b1c71d3969ac35a77cf2e Mon Sep 17 00:00:00 2001 From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com> Date: Tue, 13 Dec 2022 14:02:45 +0100 Subject: [PATCH 190/197] threads are enabled by default end forced to enabled for existing users (#7775) --- changelog.d/5503.misc | 1 + .../android/sdk/api/MatrixConfiguration.kt | 2 +- .../src/main/res/values/config-settings.xml | 2 +- .../features/home/HomeActivityViewModel.kt | 6 ++++++ .../features/settings/VectorPreferences.kt | 19 +++++++++++++++++++ .../labs/VectorSettingsLabsFragment.kt | 1 + 6 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 changelog.d/5503.misc diff --git a/changelog.d/5503.misc b/changelog.d/5503.misc new file mode 100644 index 00000000000..66deb336846 --- /dev/null +++ b/changelog.d/5503.misc @@ -0,0 +1 @@ +[Threads] - Threads Labs Flag is enabled by default and forced to be enabled for existing users, but sill can be disabled manually diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index 00d74ab446a..8c2296accb5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -66,7 +66,7 @@ data class MatrixConfiguration( /** * Thread messages default enable/disabled value. */ - val threadMessagesEnabledDefault: Boolean = false, + val threadMessagesEnabledDefault: Boolean = true, /** * List of network interceptors, they will be added when building an OkHttp client. */ diff --git a/vector-config/src/main/res/values/config-settings.xml b/vector-config/src/main/res/values/config-settings.xml index ad9c16c214d..a8695eed44d 100755 --- a/vector-config/src/main/res/values/config-settings.xml +++ b/vector-config/src/main/res/values/config-settings.xml @@ -39,7 +39,7 @@ true true - false + true true false true diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index a54ce2cff39..8f16121a306 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -254,6 +254,12 @@ class HomeActivityViewModel @AssistedInject constructor( // } when { + !vectorPreferences.areThreadMessagesEnabled() && !vectorPreferences.wasThreadFlagChangedManually() -> { + vectorPreferences.setThreadMessagesEnabled() + lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled()) + // Clear Cache + _viewEvents.post(HomeActivityViewEvents.MigrateThreads(checkSession = false)) + } // Notify users vectorPreferences.shouldNotifyUserAboutThreads() && vectorPreferences.areThreadMessagesEnabled() -> { Timber.i("----> Notify users about threads") 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 d46b819cce3..2d5fb351f9f 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 @@ -239,6 +239,7 @@ class VectorPreferences @Inject constructor( // This key will be used to identify clients with the new thread support enabled m.thread const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES_FINAL" + const val SETTINGS_LABS_THREAD_MESSAGES_CHANGED_BY_USER = "SETTINGS_LABS_THREAD_MESSAGES_CHANGED_BY_USER" const val SETTINGS_THREAD_MESSAGES_SYNCED = "SETTINGS_THREAD_MESSAGES_SYNCED" // This key will be used to enable user for displaying live user info or not. @@ -1129,6 +1130,24 @@ class VectorPreferences @Inject constructor( .apply() } + /** + * Indicates whether or not user changed threads flag manually. We need this to not force flag to be enabled on app start. + * Should be removed when Threads flag will be removed + */ + fun wasThreadFlagChangedManually(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_THREAD_MESSAGES_CHANGED_BY_USER, false) + } + + /** + * Sets the flag to indicate that user changed threads flag (e.g. disabled them). + */ + fun setThreadFlagChangedManually() { + defaultPrefs + .edit() + .putBoolean(SETTINGS_LABS_THREAD_MESSAGES_CHANGED_BY_USER, true) + .apply() + } + /** * Indicates whether or not the user will be notified about the new thread support. * We should notify the user only if he had old thread support enabled. diff --git a/vector/src/main/java/im/vector/app/features/settings/labs/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/labs/VectorSettingsLabsFragment.kt index c10411301ff..189d55d9902 100644 --- a/vector/src/main/java/im/vector/app/features/settings/labs/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/labs/VectorSettingsLabsFragment.kt @@ -141,6 +141,7 @@ class VectorSettingsLabsFragment : */ private fun onThreadsPreferenceClicked() { // We should migrate threads only if threads are disabled + vectorPreferences.setThreadFlagChangedManually() vectorPreferences.setShouldMigrateThreads(!vectorPreferences.areThreadMessagesEnabled()) lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled()) displayLoadingView() From 71df1e61d43782f7a181f267ab75e1f173dbbc0a Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 13 Dec 2022 15:45:46 +0100 Subject: [PATCH 191/197] Remove non necessary call when getting the targeted event id --- .../room/aggregation/poll/DefaultPollAggregationProcessor.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index 10c43e3b7f6..58583f8a918 100644 --- 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 @@ -78,7 +78,7 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro 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 targetEventId = event.getRelationContent()?.eventId ?: return false val targetPollContent = getPollContent(session, roomId, targetEventId) ?: return false val annotationsSummaryEntity = getAnnotationsSummaryEntity(realm, roomId, targetEventId) @@ -154,9 +154,8 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro } 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 = (event.getRelationContent() ?: content.relatesTo)?.eventId ?: return false + val pollEventId = event.getRelationContent()?.eventId ?: return false val pollOwnerId = getPollEvent(session, roomId, pollEventId)?.root?.senderId val isPollOwner = pollOwnerId == event.senderId From 96e29d4d10e4fd3a9749c714dedd31d17bb44054 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 13 Dec 2022 15:46:14 +0100 Subject: [PATCH 192/197] Renaming the name of the test file be consistent --- ...nProcessorTest.kt => DefaultPollAggregationProcessorTest.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/{PollAggregationProcessorTest.kt => DefaultPollAggregationProcessorTest.kt} (99%) 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/DefaultPollAggregationProcessorTest.kt similarity index 99% rename from matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt rename to matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt index 3044ca5d436..c1fd615e254 100644 --- 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/DefaultPollAggregationProcessorTest.kt @@ -47,7 +47,7 @@ import org.matrix.android.sdk.test.fakes.FakeRealm import org.matrix.android.sdk.test.fakes.givenEqualTo import org.matrix.android.sdk.test.fakes.givenFindFirst -class PollAggregationProcessorTest { +class DefaultPollAggregationProcessorTest { private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor() private val realm = FakeRealm() From 851276978f48811de842e36f75bf16d430dcce07 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 13 Dec 2022 15:47:30 +0100 Subject: [PATCH 193/197] Remove unused import --- .../room/aggregation/poll/DefaultPollAggregationProcessor.kt | 1 - 1 file changed, 1 deletion(-) 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 index 58583f8a918..455ccabbc62 100644 --- 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 @@ -29,7 +29,6 @@ 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 From e0a611a16edb25a036296f4763ee3da665c15903 Mon Sep 17 00:00:00 2001 From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com> Date: Wed, 14 Dec 2022 15:13:24 +0100 Subject: [PATCH 194/197] changed copy for threads labs flag (#7776) --- library/ui-strings/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 0ab1d85f0f5..60440b0dd98 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3032,7 +3032,7 @@ Auto Report Decryption Errors. Your system will automatically send logs when an unable to decrypt error occurs - Enable Thread Messages + Enable threaded messages Note: app will be restarted Show latest user info Show the latest profile info (avatar and display name) for all the messages. From cf3abd6562f37124e4996cdccbc478dea8bfcd33 Mon Sep 17 00:00:00 2001 From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com> Date: Wed, 14 Dec 2022 18:56:16 +0100 Subject: [PATCH 195/197] thread list loading (#7766) --- changelog.d/5819.misc | 1 + .../org/matrix/android/sdk/flow/FlowRoom.kt | 8 -- .../room/threads/FetchThreadsResult.kt | 23 ++++ .../api/session/room/threads/ThreadFilter.kt | 26 +++++ .../room/threads/ThreadLivePageResult.kt | 27 +++++ .../session/room/threads/ThreadsService.kt | 16 +-- .../database/RealmSessionStoreMigration.kt | 4 +- .../database/helper/ThreadSummaryHelper.kt | 24 ++-- .../database/migration/MigrateSessionTo046.kt | 32 ++++++ .../database/model/SessionRealmModule.kt | 2 + .../model/threads/ThreadListPageEntity.kt | 28 +++++ .../model/threads/ThreadSummaryEntity.kt | 3 + .../query/ThreadSummaryPageEntityQueries.kt | 31 +++++ .../sdk/internal/session/room/RoomAPI.kt | 9 ++ .../threads/FetchThreadSummariesTask.kt | 71 ++++++------ .../threads/ThreadSummariesResponse.kt | 27 +++++ .../room/threads/DefaultThreadsService.kt | 83 ++++++++++---- .../list/viewmodel/ThreadListController.kt | 66 +---------- .../viewmodel/ThreadListPagedController.kt | 84 ++++++++++++++ .../list/viewmodel/ThreadListViewModel.kt | 107 ++++++++++++++---- .../list/viewmodel/ThreadListViewState.kt | 2 - .../threads/list/views/ThreadListFragment.kt | 27 ++++- 22 files changed, 526 insertions(+), 175 deletions(-) create mode 100644 changelog.d/5819.misc create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListPagedController.kt diff --git a/changelog.d/5819.misc b/changelog.d/5819.misc new file mode 100644 index 00000000000..5f2d05dc3c3 --- /dev/null +++ b/changelog.d/5819.misc @@ -0,0 +1 @@ +[Threads] - added API to fetch threads list from the server instead of building it locally from events diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt index 7ad342b22fd..94f09e0bf5f 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -31,7 +31,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.session.room.send.UserDraft -import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional @@ -119,13 +118,6 @@ class FlowRoom(private val room: Room) { return room.roomPushRuleService().getLiveRoomNotificationState().asFlow() } - fun liveThreadSummaries(): Flow> { - return room.threadsService().getAllThreadSummariesLive().asFlow() - .startWith(room.coroutineDispatchers.io) { - room.threadsService().getAllThreadSummaries() - } - } - fun liveThreadList(): Flow> { return room.threadsLocalService().getAllThreadsLive().asFlow() .startWith(room.coroutineDispatchers.io) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt new file mode 100644 index 00000000000..5d4d67a65e7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.threads + +sealed class FetchThreadsResult { + data class ShouldFetchMore(val nextBatch: String) : FetchThreadsResult() + object ReachedEnd : FetchThreadsResult() + object Failed : FetchThreadsResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt new file mode 100644 index 00000000000..3f3576728f2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.threads + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class ThreadFilter { + @Json(name = "all") ALL, + @Json(name = "participated") PARTICIPATED, +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt new file mode 100644 index 00000000000..7693dc6fde6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.threads + +import androidx.lifecycle.LiveData +import androidx.paging.PagedList +import org.matrix.android.sdk.api.session.room.ResultBoundaries +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary + +data class ThreadLivePageResult( + val livePagedList: LiveData>, + val liveBoundaries: LiveData +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt index 9587be68f12..bb6f6b51d39 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt @@ -16,7 +16,7 @@ package org.matrix.android.sdk.api.session.room.threads -import androidx.lifecycle.LiveData +import androidx.paging.PagedList import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary /** @@ -27,15 +27,14 @@ import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary */ interface ThreadsService { - /** - * Returns a [LiveData] list of all the [ThreadSummary] that exists at the room level. - */ - fun getAllThreadSummariesLive(): LiveData> + suspend fun getPagedThreadsList(userParticipating: Boolean, pagedListConfig: PagedList.Config): ThreadLivePageResult + + suspend fun fetchThreadList(nextBatchId: String?, limit: Int, filter: ThreadFilter = ThreadFilter.ALL): FetchThreadsResult /** * Returns a list of all the [ThreadSummary] that exists at the room level. */ - fun getAllThreadSummaries(): List + suspend fun getAllThreadSummaries(): List /** * Enhance the provided ThreadSummary[List] by adding the latest @@ -51,9 +50,4 @@ interface ThreadsService { * @param limit defines the number of max results the api will respond with */ suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int) - - /** - * Fetch all thread summaries for the current room using the enhanced /messages api. - */ - suspend fun fetchThreadSummaries() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 2fb87ca8741..5295abffe32 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -62,6 +62,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo043 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo046 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -70,7 +71,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 45L, + schemaVersion = 46L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -125,5 +126,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 43) MigrateSessionTo043(realm).perform() if (oldVersion < 44) MigrateSessionTo044(realm).perform() if (oldVersion < 45) MigrateSessionTo045(realm).perform() + if (oldVersion < 46) MigrateSessionTo046(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 0ac8dc7902b..908c710df44 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 @@ -37,9 +37,11 @@ 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.RoomEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.get import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.where @@ -113,16 +115,16 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( userId: String, cryptoService: CryptoService? = null, currentTimeMillis: Long, -) { +): ThreadSummaryEntity? { when (threadSummaryType) { ThreadSummaryUpdateType.REPLACE -> { - rootThreadEvent?.eventId ?: return - rootThreadEvent.senderId ?: return + rootThreadEvent?.eventId ?: return null + rootThreadEvent.senderId ?: return null - val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return + val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return null // Something is wrong with the server return - if (numberOfThreads <= 0) return + if (numberOfThreads <= 0) return null val threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEvent.eventId).also { Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ") @@ -153,12 +155,13 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( ) roomEntity.addIfNecessary(threadSummary) + return threadSummary } ThreadSummaryUpdateType.ADD -> { - val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return + val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return null Timber.i("###THREADS ThreadSummaryHelper ADD for root eventId:$rootThreadEventId") - val threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId) + var threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId) if (threadSummary != null) { // ThreadSummary exists so lets add the latest event Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId exists, lets update latest thread event.") @@ -172,7 +175,7 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId do not exists, lets try to create one") threadEventEntity.findRootThreadEvent()?.let { rootThreadEventEntity -> // Root thread event entity exists so lets create a new record - ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).let { + threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).also { it.updateThreadSummary( rootThreadEventEntity = rootThreadEventEntity, numberOfThreads = 1, @@ -183,7 +186,12 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( roomEntity.addIfNecessary(it) } } + + threadSummary?.let { + ThreadListPageEntity.get(realm, roomId)?.threadSummaries?.add(it) + } } + return threadSummary } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt new file mode 100644 index 00000000000..4b1d2059a48 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo046(realm: DynamicRealm) : RealmMigrator(realm, 46) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.create("ThreadListPageEntity") + .addField(ThreadListPageEntityFields.ROOM_ID, String::class.java) + .addPrimaryKey(ThreadListPageEntityFields.ROOM_ID) + .setRequired(ThreadListPageEntityFields.ROOM_ID, true) + .addRealmListField(ThreadListPageEntityFields.THREAD_SUMMARIES.`$`, realm.schema.get("ThreadSummaryEntity")!!) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index 93ff67a9110..0ab30657ed4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.database.model import io.realm.annotations.RealmModule import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity /** @@ -72,6 +73,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit UserPresenceEntity::class, ThreadSummaryEntity::class, SyncFilterParamsEntity::class, + ThreadListPageEntity::class ] ) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt new file mode 100644 index 00000000000..1d64c64ddf3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model.threads + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class ThreadListPageEntity( + @PrimaryKey var roomId: String = "", + var threadSummaries: RealmList = RealmList() +) : RealmObject() { + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt index 45f9e3aa209..487be3747a0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt @@ -40,5 +40,8 @@ internal open class ThreadSummaryEntity( @LinkingObjects("threadSummaries") val room: RealmResults? = null + @LinkingObjects("threadSummaries") + val page: RealmResults? = null + companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt new file mode 100644 index 00000000000..9525e55787e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntityFields + +internal fun ThreadListPageEntity.Companion.get(realm: Realm, roomId: String): ThreadListPageEntity? { + return realm.where().equalTo(ThreadListPageEntityFields.ROOM_ID, roomId).findFirst() +} + +internal fun ThreadListPageEntity.Companion.getOrCreate(realm: Realm, roomId: String): ThreadListPageEntity { + return get(realm, roomId) ?: realm.createObject(roomId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 4e55b2c40ad..ddb7d6a8e6b 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 @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.session.room.membership.joining.InviteBod import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody import org.matrix.android.sdk.internal.session.room.read.ReadBody import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse +import org.matrix.android.sdk.internal.session.room.relation.threads.ThreadSummariesResponse import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.session.room.tags.TagBody @@ -464,4 +465,12 @@ internal interface RoomAPI { @Path("roomIdOrAlias") roomidOrAlias: String, @Query("via") viaServers: List? ): RoomStrippedState + + @GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "rooms/{roomId}/threads") + suspend fun getThreadsList( + @Path("roomId") roomId: String, + @Query("include") include: String? = "all", + @Query("from") from: String? = null, + @Query("limit") limit: Int? = null + ): ThreadSummariesResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt index 254dee42959..848b9698ee0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt @@ -16,37 +16,38 @@ package org.matrix.android.sdk.internal.session.room.relation.threads import com.zhuinden.monarchy.Monarchy +import io.realm.RealmList import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.threads.FetchThreadsResult +import org.matrix.android.sdk.api.session.room.threads.ThreadFilter import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.database.helper.createOrUpdate import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest -import org.matrix.android.sdk.internal.session.filter.FilterFactory import org.matrix.android.sdk.internal.session.room.RoomAPI -import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection -import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber import javax.inject.Inject /*** * This class is responsible to Fetch all the thread in the current room, * To fetch all threads in a room, the /messages API is used with newly added filtering options. */ -internal interface FetchThreadSummariesTask : Task { +internal interface FetchThreadSummariesTask : Task { data class Params( val roomId: String, - val from: String = "", - val limit: Int = 500, - val isUserParticipating: Boolean = true + val from: String? = null, + val limit: Int = 5, + val filter: ThreadFilter? = null, ) } @@ -59,39 +60,43 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor( private val clock: Clock, ) : FetchThreadSummariesTask { - override suspend fun execute(params: FetchThreadSummariesTask.Params): Result { - val filter = FilterFactory.createThreadsFilter( - numberOfEvents = params.limit, - userId = if (params.isUserParticipating) userId else null - ).toJSONString() - - val response = executeRequest( - globalErrorReceiver, - canRetry = true - ) { - roomAPI.getRoomMessagesFrom(params.roomId, params.from, PaginationDirection.BACKWARDS.value, params.limit, filter) + override suspend fun execute(params: FetchThreadSummariesTask.Params): FetchThreadsResult { + val response = executeRequest(globalErrorReceiver) { + roomAPI.getThreadsList( + roomId = params.roomId, + include = params.filter?.toString()?.lowercase(), + from = params.from, + limit = params.limit + ) } - Timber.i("###THREADS DefaultFetchThreadSummariesTask Fetched size:${response.events.size} nextBatch:${response.end} ") + handleResponse(response, params) - return handleResponse(response, params) + return when { + response.nextBatch != null -> FetchThreadsResult.ShouldFetchMore(response.nextBatch) + else -> FetchThreadsResult.ReachedEnd + } } private suspend fun handleResponse( - response: PaginationResponse, + response: ThreadSummariesResponse, params: FetchThreadSummariesTask.Params - ): Result { - val rootThreadList = response.events + ) { + val rootThreadList = response.chunk + + val threadSummaries = RealmList() + monarchy.awaitTransaction { realm -> val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction val roomMemberContentsByUser = HashMap() + for (rootThreadEvent in rootThreadList) { if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) { continue } - ThreadSummaryEntity.createOrUpdate( + val threadSummary = ThreadSummaryEntity.createOrUpdate( threadSummaryType = ThreadSummaryUpdateType.REPLACE, realm = realm, roomId = params.roomId, @@ -102,14 +107,16 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor( cryptoService = cryptoService, currentTimeMillis = clock.epochMillis(), ) + + threadSummaries.add(threadSummary) } - } - return Result.SUCCESS - } - enum class Result { - SHOULD_FETCH_MORE, - REACHED_END, - SUCCESS + val page = ThreadListPageEntity.getOrCreate(realm, params.roomId) + threadSummaries.forEach { + if (!page.threadSummaries.contains(it)) { + page.threadSummaries.add(it) + } + } + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt new file mode 100644 index 00000000000..d37a058ef69 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt @@ -0,0 +1,27 @@ +/* + * 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.relation.threads + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class ThreadSummariesResponse( + @Json(name = "chunk") val chunk: List, + @Json(name = "next_batch") val nextBatch: String?, + @Json(name = "prev_batch") val prevBatch: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt index 6c6d6368d1a..63756811f9d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt @@ -16,32 +16,39 @@ package org.matrix.android.sdk.internal.session.room.threads -import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.realm.Realm +import io.realm.Sort +import io.realm.kotlin.where +import org.matrix.android.sdk.api.session.room.ResultBoundaries +import org.matrix.android.sdk.api.session.room.threads.FetchThreadsResult +import org.matrix.android.sdk.api.session.room.threads.ThreadFilter +import org.matrix.android.sdk.api.session.room.threads.ThreadLivePageResult import org.matrix.android.sdk.api.session.room.threads.ThreadsService import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.internal.database.helper.enhanceWithEditions import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId import org.matrix.android.sdk.internal.database.mapper.ThreadSummaryMapper -import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.util.awaitTransaction internal class DefaultThreadsService @AssistedInject constructor( @Assisted private val roomId: String, - @UserId private val userId: String, private val fetchThreadTimelineTask: FetchThreadTimelineTask, - private val fetchThreadSummariesTask: FetchThreadSummariesTask, @SessionDatabase private val monarchy: Monarchy, - private val timelineEventMapper: TimelineEventMapper, - private val threadSummaryMapper: ThreadSummaryMapper + private val threadSummaryMapper: ThreadSummaryMapper, + private val fetchThreadSummariesTask: FetchThreadSummariesTask, ) : ThreadsService { @AssistedFactory @@ -49,16 +56,58 @@ internal class DefaultThreadsService @AssistedInject constructor( fun create(roomId: String): DefaultThreadsService } - override fun getAllThreadSummariesLive(): LiveData> { - return monarchy.findAllMappedWithChanges( - { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) }, - { - threadSummaryMapper.map(it) + override suspend fun getPagedThreadsList(userParticipating: Boolean, pagedListConfig: PagedList.Config): ThreadLivePageResult { + monarchy.awaitTransaction { realm -> + realm.where().findAll().deleteAllFromRealm() + } + + val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> + realm + .where().equalTo(ThreadSummaryEntityFields.PAGE.ROOM_ID, roomId) + .sort(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.ORIGIN_SERVER_TS, Sort.DESCENDING) + } + + val dataSourceFactory = realmDataSourceFactory.map { + threadSummaryMapper.map(it) + } + + val boundaries = MutableLiveData(ResultBoundaries()) + + val builder = LivePagedListBuilder(dataSourceFactory, pagedListConfig).also { + it.setBoundaryCallback(object : PagedList.BoundaryCallback() { + override fun onItemAtEndLoaded(itemAtEnd: ThreadSummary) { + boundaries.postValue(boundaries.value?.copy(endLoaded = true)) + } + + override fun onItemAtFrontLoaded(itemAtFront: ThreadSummary) { + boundaries.postValue(boundaries.value?.copy(frontLoaded = true)) + } + + override fun onZeroItemsLoaded() { + boundaries.postValue(boundaries.value?.copy(zeroItemLoaded = true)) } + }) + } + + val livePagedList = monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + builder ) + return ThreadLivePageResult(livePagedList, boundaries) } - override fun getAllThreadSummaries(): List { + override suspend fun fetchThreadList(nextBatchId: String?, limit: Int, filter: ThreadFilter): FetchThreadsResult { + return fetchThreadSummariesTask.execute( + FetchThreadSummariesTask.Params( + roomId = roomId, + from = nextBatchId, + limit = limit, + filter = filter + ) + ) + } + + override suspend fun getAllThreadSummaries(): List { return monarchy.fetchAllMappedSync( { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) }, { threadSummaryMapper.map(it) } @@ -81,12 +130,4 @@ internal class DefaultThreadsService @AssistedInject constructor( ) ) } - - override suspend fun fetchThreadSummaries() { - fetchThreadSummariesTask.execute( - FetchThreadSummariesTask.Params( - roomId = roomId - ) - ) - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt index 14098fd8b0f..3cbe6520761 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt @@ -19,24 +19,18 @@ package im.vector.app.features.home.room.threads.list.viewmodel import com.airbnb.epoxy.EpoxyController import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter -import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.threads.list.model.threadListItem -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.api.util.toMatrixItem -import org.matrix.android.sdk.api.util.toMatrixItemOrNull import javax.inject.Inject class ThreadListController @Inject constructor( private val avatarRenderer: AvatarRenderer, - private val stringProvider: StringProvider, private val dateFormatter: VectorDateFormatter, private val displayableEventFormatter: DisplayableEventFormatter, - private val session: Session ) : EpoxyController() { var listener: Listener? = null @@ -48,64 +42,7 @@ class ThreadListController @Inject constructor( requestModelBuild() } - override fun buildModels() = - when (session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreading) { - true -> buildThreadSummaries() - false -> buildThreadList() - } - - /** - * Building thread summaries when homeserver supports threading. - */ - private fun buildThreadSummaries() { - val safeViewState = viewState ?: return - val host = this - safeViewState.threadSummaryList.invoke() - ?.filter { - if (safeViewState.shouldFilterThreads) { - it.isUserParticipating - } else { - true - } - } - ?.forEach { threadSummary -> - val date = dateFormatter.format(threadSummary.latestEvent?.originServerTs, DateFormatKind.ROOM_LIST) - val lastMessageFormatted = threadSummary.let { - displayableEventFormatter.formatThreadSummary( - event = it.latestEvent, - latestEdition = it.threadEditions.latestThreadEdition - ).toString() - } - val rootMessageFormatted = threadSummary.let { - displayableEventFormatter.formatThreadSummary( - event = it.rootEvent, - latestEdition = it.threadEditions.rootThreadEdition - ).toString() - } - threadListItem { - id(threadSummary.rootEvent?.eventId) - avatarRenderer(host.avatarRenderer) - matrixItem(threadSummary.rootThreadSenderInfo.toMatrixItem()) - title(threadSummary.rootThreadSenderInfo.displayName.orEmpty()) - date(date) - rootMessageDeleted(threadSummary.rootEvent?.isRedacted() ?: false) - // TODO refactor notifications that with the new thread summary - threadNotificationState(threadSummary.rootEvent?.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE) - rootMessage(rootMessageFormatted) - lastMessage(lastMessageFormatted) - lastMessageCounter(threadSummary.numberOfThreads.toString()) - lastMessageMatrixItem(threadSummary.latestThreadSenderInfo.toMatrixItemOrNull()) - itemClickListener { - host.listener?.onThreadSummaryClicked(threadSummary) - } - } - } - } - - /** - * Building local thread list when homeserver do not support threading. - */ - private fun buildThreadList() { + override fun buildModels() { val safeViewState = viewState ?: return val host = this safeViewState.rootThreadEventList.invoke() @@ -152,7 +89,6 @@ class ThreadListController @Inject constructor( } interface Listener { - fun onThreadSummaryClicked(threadSummary: ThreadSummary) fun onThreadListClicked(timelineEvent: TimelineEvent) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListPagedController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListPagedController.kt new file mode 100644 index 00000000000..171b690a33b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListPagedController.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.threads.list.viewmodel + +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.paging.PagedListEpoxyController +import im.vector.app.core.date.DateFormatKind +import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.utils.createUIHandler +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter +import im.vector.app.features.home.room.threads.list.model.ThreadListItem_ +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary +import org.matrix.android.sdk.api.session.threads.ThreadNotificationState +import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.api.util.toMatrixItemOrNull +import javax.inject.Inject + +class ThreadListPagedController @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val dateFormatter: VectorDateFormatter, + private val displayableEventFormatter: DisplayableEventFormatter, +) : PagedListEpoxyController( + // Important it must match the PageList builder notify Looper + modelBuildingHandler = createUIHandler() +) { + + var listener: Listener? = null + + override fun buildItemModel(currentPosition: Int, item: ThreadSummary?): EpoxyModel<*> { + if (item == null) { + throw java.lang.NullPointerException() + } + val host = this + val date = dateFormatter.format(item.latestEvent?.originServerTs, DateFormatKind.ROOM_LIST) + val lastMessageFormatted = item.let { + displayableEventFormatter.formatThreadSummary( + event = it.latestEvent, + latestEdition = it.threadEditions.latestThreadEdition + ).toString() + } + val rootMessageFormatted = item.let { + displayableEventFormatter.formatThreadSummary( + event = it.rootEvent, + latestEdition = it.threadEditions.rootThreadEdition + ).toString() + } + + return ThreadListItem_() + .id(item.rootEvent?.eventId) + .avatarRenderer(host.avatarRenderer) + .matrixItem(item.rootThreadSenderInfo.toMatrixItem()) + .title(item.rootThreadSenderInfo.displayName.orEmpty()) + .date(date) + .rootMessageDeleted(item.rootEvent?.isRedacted() ?: false) + // TODO refactor notifications that with the new thread summary + .threadNotificationState(item.rootEvent?.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE) + .rootMessage(rootMessageFormatted) + .lastMessage(lastMessageFormatted) + .lastMessageCounter(item.numberOfThreads.toString()) + .lastMessageMatrixItem(item.latestThreadSenderInfo.toMatrixItemOrNull()) + .itemClickListener { + host.listener?.onThreadSummaryClicked(item) + } + } + + interface Listener { + fun onThreadSummaryClicked(threadSummary: ThreadSummary) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt index 4b7af330fb5..7124727bb76 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt @@ -16,6 +16,11 @@ package im.vector.app.features.home.room.threads.list.viewmodel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.asFlow +import androidx.paging.PagedList import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.ViewModelContext @@ -29,23 +34,47 @@ import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsInteraction import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.home.room.threads.list.views.ThreadListFragment +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.threads.FetchThreadsResult +import org.matrix.android.sdk.api.session.room.threads.ThreadFilter +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent import org.matrix.android.sdk.flow.flow class ThreadListViewModel @AssistedInject constructor( @Assisted val initialState: ThreadListViewState, private val analyticsTracker: AnalyticsTracker, - private val session: Session -) : - VectorViewModel(initialState) { + private val session: Session, +) : VectorViewModel(initialState) { private val room = session.getRoom(initialState.roomId) + private val defaultPagedListConfig = PagedList.Config.Builder() + .setPageSize(20) + .setInitialLoadSizeHint(40) + .setEnablePlaceholders(false) + .setPrefetchDistance(10) + .build() + + private var nextBatchId: String? = null + private var hasReachedEnd: Boolean = false + private var boundariesJob: Job? = null + + private var livePagedList: LiveData>? = null + private val _threadsLivePagedList = MutableLiveData>() + val threadsLivePagedList: LiveData> = _threadsLivePagedList + private val internalPagedListObserver = Observer> { + _threadsLivePagedList.postValue(it) + setLoading(false) + } + @AssistedFactory interface Factory { fun create(initialState: ThreadListViewState): ThreadListViewModel @@ -54,7 +83,7 @@ class ThreadListViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory { @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: ThreadListViewState): ThreadListViewModel? { + override fun create(viewModelContext: ViewModelContext, state: ThreadListViewState): ThreadListViewModel { val fragment: ThreadListFragment = (viewModelContext as FragmentViewModelContext).fragment() return fragment.threadListViewModelFactory.create(state) } @@ -72,7 +101,7 @@ class ThreadListViewModel @AssistedInject constructor( private fun fetchAndObserveThreads() { when (session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreading) { true -> { - fetchThreadList() + setLoading(true) observeThreadSummaries() } false -> observeThreadsList() @@ -82,14 +111,33 @@ class ThreadListViewModel @AssistedInject constructor( /** * Observing thread summaries when homeserver support threading. */ - private fun observeThreadSummaries() { - room?.flow() - ?.liveThreadSummaries() - ?.map { room.threadsService().enhanceThreadWithEditions(it) } - ?.flowOn(room.coroutineDispatchers.io) - ?.execute { asyncThreads -> - copy(threadSummaryList = asyncThreads) - } + private fun observeThreadSummaries() = withState { state -> + viewModelScope.launch { + nextBatchId = null + hasReachedEnd = false + + livePagedList?.removeObserver(internalPagedListObserver) + + room?.threadsService() + ?.getPagedThreadsList(state.shouldFilterThreads, defaultPagedListConfig)?.let { result -> + livePagedList = result.livePagedList + + livePagedList?.observeForever(internalPagedListObserver) + + boundariesJob = result.liveBoundaries.asFlow() + .onEach { + if (it.endLoaded) { + if (!hasReachedEnd) { + fetchNextPage() + } + } + } + .launchIn(viewModelScope) + } + + setLoading(true) + fetchNextPage() + } } /** @@ -111,14 +159,6 @@ class ThreadListViewModel @AssistedInject constructor( } } - private fun fetchThreadList() { - viewModelScope.launch { - setLoading(true) - room?.threadsService()?.fetchThreadSummaries() - setLoading(false) - } - } - private fun setLoading(isLoading: Boolean) { setState { copy(isLoading = isLoading) @@ -132,5 +172,30 @@ class ThreadListViewModel @AssistedInject constructor( setState { copy(shouldFilterThreads = shouldFilterThreads) } + + fetchAndObserveThreads() + } + + private suspend fun fetchNextPage() { + val filter = when (awaitState().shouldFilterThreads) { + true -> ThreadFilter.PARTICIPATED + false -> ThreadFilter.ALL + } + room?.threadsService()?.fetchThreadList( + nextBatchId = nextBatchId, + limit = defaultPagedListConfig.pageSize, + filter = filter, + ).let { result -> + when (result) { + is FetchThreadsResult.ReachedEnd -> { + hasReachedEnd = true + } + is FetchThreadsResult.ShouldFetchMore -> { + nextBatchId = result.nextBatch + } + else -> { + } + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt index 2328da0b8a2..60ccfb59afb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt @@ -20,11 +20,9 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.home.room.threads.arguments.ThreadListArgs -import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent data class ThreadListViewState( - val threadSummaryList: Async> = Uninitialized, val rootThreadEventList: Async> = Uninitialized, val shouldFilterThreads: Boolean = false, val isLoading: Boolean = false, diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt index f91fe9bd915..318c2509060 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt @@ -40,6 +40,7 @@ import im.vector.app.features.home.room.threads.ThreadsActivity import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController +import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListPagedController import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState import im.vector.app.features.rageshake.BugReporter @@ -52,12 +53,14 @@ import javax.inject.Inject @AndroidEntryPoint class ThreadListFragment : VectorBaseFragment(), + ThreadListPagedController.Listener, ThreadListController.Listener, VectorMenuProvider { @Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var bugReporter: BugReporter - @Inject lateinit var threadListController: ThreadListController + @Inject lateinit var threadListController: ThreadListPagedController + @Inject lateinit var legacyThreadListController: ThreadListController @Inject lateinit var threadListViewModelFactory: ThreadListViewModel.Factory private val threadListViewModel: ThreadListViewModel by fragmentViewModel() @@ -100,7 +103,7 @@ class ThreadListFragment : val filterBadge = filterIcon.findViewById(R.id.threadListFilterBadge) filterBadge.isVisible = state.shouldFilterThreads when (threadListViewModel.canHomeserverUseThreading()) { - true -> menu.findItem(R.id.menu_thread_list_filter).isVisible = !state.threadSummaryList.invoke().isNullOrEmpty() + true -> menu.findItem(R.id.menu_thread_list_filter).isVisible = true false -> menu.findItem(R.id.menu_thread_list_filter).isVisible = !state.rootThreadEventList.invoke().isNullOrEmpty() } } @@ -111,8 +114,18 @@ class ThreadListFragment : initToolbar() initTextConstants() initBetaFeedback() - views.threadListRecyclerView.configureWith(threadListController, TimelineItemAnimator(), hasFixedSize = false) - threadListController.listener = this + + if (threadListViewModel.canHomeserverUseThreading()) { + views.threadListRecyclerView.configureWith(threadListController, TimelineItemAnimator(), hasFixedSize = false) + threadListController.listener = this + + threadListViewModel.threadsLivePagedList.observe(viewLifecycleOwner) { threadsList -> + threadListController.submitList(threadsList) + } + } else { + views.threadListRecyclerView.configureWith(legacyThreadListController, TimelineItemAnimator(), hasFixedSize = false) + legacyThreadListController.listener = this + } } override fun onDestroyView() { @@ -144,7 +157,9 @@ class ThreadListFragment : override fun invalidate() = withState(threadListViewModel) { state -> invalidateOptionsMenu() renderEmptyStateIfNeeded(state) - threadListController.update(state) + if (!threadListViewModel.canHomeserverUseThreading()) { + legacyThreadListController.update(state) + } renderLoaderIfNeeded(state) } @@ -185,7 +200,7 @@ class ThreadListFragment : private fun renderEmptyStateIfNeeded(state: ThreadListViewState) { when (threadListViewModel.canHomeserverUseThreading()) { - true -> views.threadListEmptyConstraintLayout.isVisible = state.threadSummaryList.invoke().isNullOrEmpty() + true -> views.threadListEmptyConstraintLayout.isVisible = false false -> views.threadListEmptyConstraintLayout.isVisible = state.rootThreadEventList.invoke().isNullOrEmpty() } } From 82ad08aced2979b6c37459dd6d965260a80f23dc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Dec 2022 10:15:24 +0100 Subject: [PATCH 196/197] Changelog for version 1.5.12 --- CHANGES.md | 51 ++++++++++++++++++++++++++++++++++++++++ changelog.d/5503.misc | 1 - changelog.d/5819.misc | 1 - changelog.d/7274.bugfix | 1 - changelog.d/7477.misc | 1 - changelog.d/7596.feature | 1 - changelog.d/7632.feature | 1 - changelog.d/7643.bugfix | 1 - changelog.d/7645.misc | 1 - changelog.d/7653.bugfix | 1 - changelog.d/7658.bugfix | 1 - changelog.d/7659.bugfix | 1 - changelog.d/7680.bugfix | 3 --- changelog.d/7683.bugfix | 2 -- changelog.d/7684.bugfix | 1 - changelog.d/7691.bugfix | 1 - changelog.d/7693.feature | 1 - changelog.d/7694.feature | 1 - changelog.d/7695.bugfix | 1 - changelog.d/7697.feature | 1 - changelog.d/7699.bugfix | 1 - changelog.d/7708.misc | 1 - changelog.d/7710.bugfix | 1 - changelog.d/7719.feature | 1 - changelog.d/7723.misc | 1 - changelog.d/7725.bugfix | 1 - changelog.d/7733.bugfix | 1 - changelog.d/7737.bugfix | 1 - changelog.d/7740.feature | 1 - changelog.d/7743.bugfix | 1 - changelog.d/7744.bugfix | 1 - changelog.d/7751.bugfix | 1 - changelog.d/7753.bugfix | 1 - changelog.d/7754.feature | 1 - changelog.d/7770.bugfix | 1 - 35 files changed, 51 insertions(+), 37 deletions(-) delete mode 100644 changelog.d/5503.misc delete mode 100644 changelog.d/5819.misc delete mode 100644 changelog.d/7274.bugfix delete mode 100644 changelog.d/7477.misc delete mode 100644 changelog.d/7596.feature delete mode 100644 changelog.d/7632.feature delete mode 100644 changelog.d/7643.bugfix delete mode 100644 changelog.d/7645.misc delete mode 100644 changelog.d/7653.bugfix delete mode 100644 changelog.d/7658.bugfix delete mode 100644 changelog.d/7659.bugfix delete mode 100644 changelog.d/7680.bugfix delete mode 100644 changelog.d/7683.bugfix delete mode 100644 changelog.d/7684.bugfix delete mode 100644 changelog.d/7691.bugfix delete mode 100644 changelog.d/7693.feature delete mode 100644 changelog.d/7694.feature delete mode 100644 changelog.d/7695.bugfix delete mode 100644 changelog.d/7697.feature delete mode 100644 changelog.d/7699.bugfix delete mode 100644 changelog.d/7708.misc delete mode 100644 changelog.d/7710.bugfix delete mode 100644 changelog.d/7719.feature delete mode 100644 changelog.d/7723.misc delete mode 100644 changelog.d/7725.bugfix delete mode 100644 changelog.d/7733.bugfix delete mode 100644 changelog.d/7737.bugfix delete mode 100644 changelog.d/7740.feature delete mode 100644 changelog.d/7743.bugfix delete mode 100644 changelog.d/7744.bugfix delete mode 100644 changelog.d/7751.bugfix delete mode 100644 changelog.d/7753.bugfix delete mode 100644 changelog.d/7754.feature delete mode 100644 changelog.d/7770.bugfix diff --git a/CHANGES.md b/CHANGES.md index c170c3b92bb..0481ec1af60 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,54 @@ +Changes in Element v1.5.12 (2022-12-15) +======================================= + +Features ✨ +---------- +- [Threads] - Threads Labs Flag is enabled by default and forced to be enabled for existing users, but sill can be disabled manually ([#5503](https://github.com/vector-im/element-android/issues/5503)) + - [Session manager] Add action to signout all the other session ([#7693](https://github.com/vector-im/element-android/issues/7693)) + - Remind unverified sessions with a banner once a week ([#7694](https://github.com/vector-im/element-android/issues/7694)) + - [Session manager] Add actions to rename and signout current session ([#7697](https://github.com/vector-im/element-android/issues/7697)) + - Voice Broadcast - Update last message in the room list ([#7719](https://github.com/vector-im/element-android/issues/7719)) + - Delete unused client information from account data ([#7754](https://github.com/vector-im/element-android/issues/7754)) + +Bugfixes 🐛 +---------- + - Fix bad pills color background. For light and dark theme the color is now 61708B (iso EleWeb) ([#7274](https://github.com/vector-im/element-android/issues/7274)) + - [Notifications] Fixed a bug when push notification was automatically dismissed while app is on background ([#7643](https://github.com/vector-im/element-android/issues/7643)) + - ANR when asking to select the notification method ([#7653](https://github.com/vector-im/element-android/issues/7653)) + - [Rich text editor] Fix design and spacing of rich text editor ([#7658](https://github.com/vector-im/element-android/issues/7658)) + - [Rich text editor] Fix keyboard closing after collapsing editor ([#7659](https://github.com/vector-im/element-android/issues/7659)) + - Rich Text Editor: fix several issues related to insets: + * Empty space displayed at the bottom when you don't have permissions to send messages into a room. + * Wrong insets being kept when you exit the room screen and the keyboard is displayed, then come back to it. ([#7680](https://github.com/vector-im/element-android/issues/7680)) + - Fix crash in message composer when room is missing ([#7683](https://github.com/vector-im/element-android/issues/7683)) + - Fix crash when invalid homeserver url is entered. ([#7684](https://github.com/vector-im/element-android/issues/7684)) + - Rich Text Editor: improve performance when entering reply/edit/quote mode. ([#7691](https://github.com/vector-im/element-android/issues/7691)) + - [Rich text editor] Add error tracking for rich text editor ([#7695](https://github.com/vector-im/element-android/issues/7695)) + - Fix E2EE set up failure whilst signing in using QR code ([#7699](https://github.com/vector-im/element-android/issues/7699)) + - Fix usage of unknown shield in room summary ([#7710](https://github.com/vector-im/element-android/issues/7710)) + - Fix crash when the network is not available. ([#7725](https://github.com/vector-im/element-android/issues/7725)) + - [Session manager] Sessions without encryption support should not prompt to verify ([#7733](https://github.com/vector-im/element-android/issues/7733)) + - Fix issue of Scan QR code button sometimes not showing when it should be available ([#7737](https://github.com/vector-im/element-android/issues/7737)) + - Verification request is not showing when verify session popup is displayed ([#7743](https://github.com/vector-im/element-android/issues/7743)) + - Fix crash when inviting by email. ([#7744](https://github.com/vector-im/element-android/issues/7744)) + - Revert usage of stable fields in live location sharing and polls ([#7751](https://github.com/vector-im/element-android/issues/7751)) + - [Poll] Poll end event is not recognized ([#7753](https://github.com/vector-im/element-android/issues/7753)) + - [Push Notifications] When push notification for threaded message is clicked, thread timeline will be opened instead of room's main timeline ([#7770](https://github.com/vector-im/element-android/issues/7770)) + +Other changes +------------- + - [Threads] - added API to fetch threads list from the server instead of building it locally from events ([#5819](https://github.com/vector-im/element-android/issues/5819)) + - Add Z-Labs label for rich text editor and migrate to new label naming. ([#7477](https://github.com/vector-im/element-android/issues/7477)) + - Crypto database migration tests ([#7645](https://github.com/vector-im/element-android/issues/7645)) + - Add tracing Id for to device messages ([#7708](https://github.com/vector-im/element-android/issues/7708)) + - Disable nightly popup and add an entry point in the advanced settings instead. ([#7723](https://github.com/vector-im/element-android/issues/7723)) +- Save m.local_notification_settings. event in account_data ([#7596](https://github.com/vector-im/element-android/issues/7596)) +- Update notifications setting when m.local_notification_settings. event changes for current device ([#7632](https://github.com/vector-im/element-android/issues/7632)) + +SDK API changes ⚠️ +------------------ +- Handle account data removal ([#7740](https://github.com/vector-im/element-android/issues/7740)) + Changes in Element 1.5.11 (2022-12-07) ====================================== diff --git a/changelog.d/5503.misc b/changelog.d/5503.misc deleted file mode 100644 index 66deb336846..00000000000 --- a/changelog.d/5503.misc +++ /dev/null @@ -1 +0,0 @@ -[Threads] - Threads Labs Flag is enabled by default and forced to be enabled for existing users, but sill can be disabled manually diff --git a/changelog.d/5819.misc b/changelog.d/5819.misc deleted file mode 100644 index 5f2d05dc3c3..00000000000 --- a/changelog.d/5819.misc +++ /dev/null @@ -1 +0,0 @@ -[Threads] - added API to fetch threads list from the server instead of building it locally from events diff --git a/changelog.d/7274.bugfix b/changelog.d/7274.bugfix deleted file mode 100644 index e99daceb897..00000000000 --- a/changelog.d/7274.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bad pills color background. For light and dark theme the color is now 61708B (iso EleWeb) diff --git a/changelog.d/7477.misc b/changelog.d/7477.misc deleted file mode 100644 index 2ea83ce81d5..00000000000 --- a/changelog.d/7477.misc +++ /dev/null @@ -1 +0,0 @@ -Add Z-Labs label for rich text editor and migrate to new label naming. \ No newline at end of file diff --git a/changelog.d/7596.feature b/changelog.d/7596.feature deleted file mode 100644 index 022d86342b5..00000000000 --- a/changelog.d/7596.feature +++ /dev/null @@ -1 +0,0 @@ -Save m.local_notification_settings. event in account_data diff --git a/changelog.d/7632.feature b/changelog.d/7632.feature deleted file mode 100644 index 460f9877567..00000000000 --- a/changelog.d/7632.feature +++ /dev/null @@ -1 +0,0 @@ -Update notifications setting when m.local_notification_settings. event changes for current device diff --git a/changelog.d/7643.bugfix b/changelog.d/7643.bugfix deleted file mode 100644 index 66e3f28d5f1..00000000000 --- a/changelog.d/7643.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Notifications] Fixed a bug when push notification was automatically dismissed while app is on background diff --git a/changelog.d/7645.misc b/changelog.d/7645.misc deleted file mode 100644 index a133581ac12..00000000000 --- a/changelog.d/7645.misc +++ /dev/null @@ -1 +0,0 @@ -Crypto database migration tests diff --git a/changelog.d/7653.bugfix b/changelog.d/7653.bugfix deleted file mode 100644 index ae49c4ed4e6..00000000000 --- a/changelog.d/7653.bugfix +++ /dev/null @@ -1 +0,0 @@ -ANR when asking to select the notification method diff --git a/changelog.d/7658.bugfix b/changelog.d/7658.bugfix deleted file mode 100644 index a5ab85b1912..00000000000 --- a/changelog.d/7658.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Rich text editor] Fix design and spacing of rich text editor diff --git a/changelog.d/7659.bugfix b/changelog.d/7659.bugfix deleted file mode 100644 index 38be1008ef7..00000000000 --- a/changelog.d/7659.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Rich text editor] Fix keyboard closing after collapsing editor diff --git a/changelog.d/7680.bugfix b/changelog.d/7680.bugfix deleted file mode 100644 index 2e3b4b2e48a..00000000000 --- a/changelog.d/7680.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -Rich Text Editor: fix several issues related to insets: -* Empty space displayed at the bottom when you don't have permissions to send messages into a room. -* Wrong insets being kept when you exit the room screen and the keyboard is displayed, then come back to it. diff --git a/changelog.d/7683.bugfix b/changelog.d/7683.bugfix deleted file mode 100644 index 3922253ba6f..00000000000 --- a/changelog.d/7683.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Fix crash in message composer when room is missing - diff --git a/changelog.d/7684.bugfix b/changelog.d/7684.bugfix deleted file mode 100644 index 4a9af884a11..00000000000 --- a/changelog.d/7684.bugfix +++ /dev/null @@ -1 +0,0 @@ - Fix crash when invalid homeserver url is entered. diff --git a/changelog.d/7691.bugfix b/changelog.d/7691.bugfix deleted file mode 100644 index 02988191434..00000000000 --- a/changelog.d/7691.bugfix +++ /dev/null @@ -1 +0,0 @@ -Rich Text Editor: improve performance when entering reply/edit/quote mode. diff --git a/changelog.d/7693.feature b/changelog.d/7693.feature deleted file mode 100644 index 271964db82f..00000000000 --- a/changelog.d/7693.feature +++ /dev/null @@ -1 +0,0 @@ -[Session manager] Add action to signout all the other session diff --git a/changelog.d/7694.feature b/changelog.d/7694.feature deleted file mode 100644 index 408925974e0..00000000000 --- a/changelog.d/7694.feature +++ /dev/null @@ -1 +0,0 @@ -Remind unverified sessions with a banner once a week diff --git a/changelog.d/7695.bugfix b/changelog.d/7695.bugfix deleted file mode 100644 index 7ec0805bce6..00000000000 --- a/changelog.d/7695.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Rich text editor] Add error tracking for rich text editor diff --git a/changelog.d/7697.feature b/changelog.d/7697.feature deleted file mode 100644 index 6d71a84a40b..00000000000 --- a/changelog.d/7697.feature +++ /dev/null @@ -1 +0,0 @@ -[Session manager] Add actions to rename and signout current session diff --git a/changelog.d/7699.bugfix b/changelog.d/7699.bugfix deleted file mode 100644 index 30a4b8e9fad..00000000000 --- a/changelog.d/7699.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix E2EE set up failure whilst signing in using QR code diff --git a/changelog.d/7708.misc b/changelog.d/7708.misc deleted file mode 100644 index 62733303955..00000000000 --- a/changelog.d/7708.misc +++ /dev/null @@ -1 +0,0 @@ -Add tracing Id for to device messages diff --git a/changelog.d/7710.bugfix b/changelog.d/7710.bugfix deleted file mode 100644 index 9e75a03e1b9..00000000000 --- a/changelog.d/7710.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix usage of unknown shield in room summary diff --git a/changelog.d/7719.feature b/changelog.d/7719.feature deleted file mode 100644 index 34df6ad964f..00000000000 --- a/changelog.d/7719.feature +++ /dev/null @@ -1 +0,0 @@ -Voice Broadcast - Update last message in the room list diff --git a/changelog.d/7723.misc b/changelog.d/7723.misc deleted file mode 100644 index 36869d1efbb..00000000000 --- a/changelog.d/7723.misc +++ /dev/null @@ -1 +0,0 @@ -Disable nightly popup and add an entry point in the advanced settings instead. diff --git a/changelog.d/7725.bugfix b/changelog.d/7725.bugfix deleted file mode 100644 index b701451505f..00000000000 --- a/changelog.d/7725.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix crash when the network is not available. diff --git a/changelog.d/7733.bugfix b/changelog.d/7733.bugfix deleted file mode 100644 index 9de3759f1ac..00000000000 --- a/changelog.d/7733.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Session manager] Sessions without encryption support should not prompt to verify diff --git a/changelog.d/7737.bugfix b/changelog.d/7737.bugfix deleted file mode 100644 index 14778346743..00000000000 --- a/changelog.d/7737.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix issue of Scan QR code button sometimes not showing when it should be available diff --git a/changelog.d/7740.feature b/changelog.d/7740.feature deleted file mode 100644 index 6cd2b6c7764..00000000000 --- a/changelog.d/7740.feature +++ /dev/null @@ -1 +0,0 @@ -Handle account data removal diff --git a/changelog.d/7743.bugfix b/changelog.d/7743.bugfix deleted file mode 100644 index 867c12a3c30..00000000000 --- a/changelog.d/7743.bugfix +++ /dev/null @@ -1 +0,0 @@ -Verification request is not showing when verify session popup is displayed diff --git a/changelog.d/7744.bugfix b/changelog.d/7744.bugfix deleted file mode 100644 index 7ed82a9c1c6..00000000000 --- a/changelog.d/7744.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix crash when inviting by email. diff --git a/changelog.d/7751.bugfix b/changelog.d/7751.bugfix deleted file mode 100644 index 5d676dbc4d2..00000000000 --- a/changelog.d/7751.bugfix +++ /dev/null @@ -1 +0,0 @@ -Revert usage of stable fields in live location sharing and polls diff --git a/changelog.d/7753.bugfix b/changelog.d/7753.bugfix deleted file mode 100644 index 10579b6a845..00000000000 --- a/changelog.d/7753.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Poll] Poll end event is not recognized diff --git a/changelog.d/7754.feature b/changelog.d/7754.feature deleted file mode 100644 index 0e1b6d09617..00000000000 --- a/changelog.d/7754.feature +++ /dev/null @@ -1 +0,0 @@ -Delete unused client information from account data diff --git a/changelog.d/7770.bugfix b/changelog.d/7770.bugfix deleted file mode 100644 index 598deb60735..00000000000 --- a/changelog.d/7770.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Push Notifications] When push notification for threaded message is clicked, thread timeline will be opened instead of room's main timeline From dd88ac597e60d9011e603116d2796e5a505a2e74 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Dec 2022 10:16:02 +0100 Subject: [PATCH 197/197] Adding fastlane file for version 1.5.12 --- fastlane/metadata/android/en-US/changelogs/40105120.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/40105120.txt diff --git a/fastlane/metadata/android/en-US/changelogs/40105120.txt b/fastlane/metadata/android/en-US/changelogs/40105120.txt new file mode 100644 index 00000000000..91c25cf053b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Main changes in this version: Thread are now enabled by default. +Full changelog: https://github.com/vector-im/element-android/releases