From 06ee7b791b6a9237b2503cc7b3361e3ee34a5e5a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Mar 2023 11:36:23 +0200 Subject: [PATCH 001/104] Add string key naming rules. --- tools/localazy/README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tools/localazy/README.md b/tools/localazy/README.md index dcb45c5be7a..af040d17f74 100644 --- a/tools/localazy/README.md +++ b/tools/localazy/README.md @@ -4,10 +4,22 @@ Localazy is used to host the source strings and their translations. ## Localazy project -To add new strings, or to translate existing strings, go the the Localazy project: [https://localazy.com/p/element](https://localazy.com/p/element). +To add new strings, or to translate existing strings, go the the Localazy project: [https://localazy.com/p/element](https://localazy.com/p/element). Please follow the key naming rules (see below). Never edit manually the files `localazy.xml` or `translations.xml`!. +### Key naming rules + +For code clarity and in order to download strings to the correct module, here are some naming rules to follow as much as possible: + +- Keys for common strings, i.e. strings that can be used at multiple places must start by `action_` if this is a verb, or `common_` if not; +- Keys for strings used in a single screen must start with `screen_` followed by the screen name, followed by a free name. Example: `screen_onboarding_welcome_title`; +- Keys can have `_title` or `_subtitle` suffixes. Example: `screen_onboarding_welcome_title`, `screen_change_server_subtitle`; +- `a11y_` pattern can be used for strings used for accessibility. Example: `a11y_hide_password`, `screen_roomlist_a11y_create_message`; +- Strings for error message can start by `error_`, or contain `_error_` if used in a specific screen only. Example: `error_some_messages_have_not_been_sent`, `screen_change_server_error_invalid_homeserver`. + +*Note*: those rules applies for `strings` and for `plurals`. + ## CLI Installation To install the Localazy client, follow the instructions from [here](https://localazy.com/docs/cli/installation). From 1d6e1565d798bfc6e82e7710471ac666c2b332e0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Mar 2023 14:03:52 +0200 Subject: [PATCH 002/104] Update documentation about strings. --- CONTRIBUTING.md | 32 ++++++++++++++++++++++---------- tools/localazy/README.md | 10 ++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5cd8785b33f..0c462036b90 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,9 @@ * [Contributing code to Matrix](#contributing-code-to-matrix) * [Android Studio settings](#android-studio-settings) * [Compilation](#compilation) -* [I want to help translating Element](#i-want-to-help-translating-element) +* [Strings](#strings) + * [I want to add new strings to the project](#i-want-to-add-new-strings-to-the-project) + * [I want to help translating Element](#i-want-to-help-translating-element) * [I want to submit a PR to fix an issue](#i-want-to-submit-a-pr-to-fix-an-issue) * [Kotlin](#kotlin) * [Changelog](#changelog) @@ -15,7 +17,6 @@ * [lint](#lint) * [Unit tests](#unit-tests) * [Tests](#tests) - * [Internationalisation](#internationalisation) * [Accessibility](#accessibility) * [Jetpack Compose](#jetpack-compose) * [Authors](#authors) @@ -40,11 +41,26 @@ Please ensure that you're using the project formatting rules (which are in the p This project should compile without any special action. Just clone it and open it with Android Studio, or compile from command line using `gradlew`. -## I want to help translating Element +## Strings -For now strings are coming from Element Android project, so: -- If you want to fix an issue with an English string, please submit a PR on Element Android. -- If you want to fix an issue in other languages, or add a missing translation, or even add a new language, please use [Weblate](https://translate.element.io/projects/element-android/). +The strings of the project are managed externally using [https://localazy.com](https://localazy.com) and shared with ElementX iOS. + +### I want to add new strings to the project + +Only the core team can modify or add English strings to Localazy. As an external contributor, if you want to add new strings, feel free to add an Android resource file to the project (for instance a file named `temporary.xml`), with a note in the description of the PR for the reviewer to integrate the String into `Localazy`. If accepted, the reviewer will add the String(s) for you, and then you can download them on your branch (following these [instructions](./tools/localazy/README.md#download-translations)) and remove the temporary file. + +Please follow the naming rules for the key. More details in [the dedicated section in this README.md](./tools/localazy/README.md#key-naming-rules) + +### I want to help translating Element + +Please note that the Localazy project is not open yet for external contributions. + +To help translating, please go to [https://localazy.com/p/element](https://localazy.com/p/element). + +- If you want to fix an issue with an English string, please open an issue on the github project of ElementX (Android or iOS).Only the core team can modify or add English strings. +- If you want to fix an issue in other languages, or add a missing translation, or even add a new language, please go to [https://localazy.com/p/element](https://localazy.com/p/element). + +More informations can be found [in this README.md](./tools/localazy/README.md). ## I want to submit a PR to fix an issue @@ -135,10 +151,6 @@ Also, if possible, please test your change on a real device. Testing on Android You should consider adding Unit tests with your PR, and also integration tests (AndroidTest). Please refer to [this document](./docs/integration_tests.md) to install and run the integration test environment. -### Internationalisation - -For now strings are coming from Element Android project, so please read [the documentation](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#internationalisation) from there. - ### Accessibility Please consider accessibility as an important point. As a minimum requirement, in layout XML files please use attributes such as `android:contentDescription` and `android:importantForAccessibility`, and test with a screen reader if it's working well. You can add new string resources, dedicated to accessibility, in this case, please prefix theirs id with `a11y_`. diff --git a/tools/localazy/README.md b/tools/localazy/README.md index af040d17f74..e855b339924 100644 --- a/tools/localazy/README.md +++ b/tools/localazy/README.md @@ -2,6 +2,16 @@ Localazy is used to host the source strings and their translations. + + +* [Localazy project](#localazy-project) + * [Key naming rules](#key-naming-rules) +* [CLI Installation](#cli-installation) +* [Download translations](#download-translations) +* [Add translations to a specific module](#add-translations-to-a-specific-module) + + + ## Localazy project To add new strings, or to translate existing strings, go the the Localazy project: [https://localazy.com/p/element](https://localazy.com/p/element). Please follow the key naming rules (see below). From 9b05010a5610e15834e2244671bb8c31647a22ea Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Mar 2023 14:04:17 +0200 Subject: [PATCH 003/104] Add a note about the configuration. --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c462036b90..82602e7e632 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,6 +41,8 @@ Please ensure that you're using the project formatting rules (which are in the p This project should compile without any special action. Just clone it and open it with Android Studio, or compile from command line using `gradlew`. +Note: please make sure that the configuration is `app` and not `samples.minimal`. + ## Strings The strings of the project are managed externally using [https://localazy.com](https://localazy.com) and shared with ElementX iOS. From 0a2bfd220d172a02505c27a95c1bf5e1d7eddad5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Mar 2023 14:05:50 +0200 Subject: [PATCH 004/104] Small clarification --- tools/localazy/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/localazy/README.md b/tools/localazy/README.md index e855b339924..6117cf17c07 100644 --- a/tools/localazy/README.md +++ b/tools/localazy/README.md @@ -25,7 +25,7 @@ For code clarity and in order to download strings to the correct module, here ar - Keys for common strings, i.e. strings that can be used at multiple places must start by `action_` if this is a verb, or `common_` if not; - Keys for strings used in a single screen must start with `screen_` followed by the screen name, followed by a free name. Example: `screen_onboarding_welcome_title`; - Keys can have `_title` or `_subtitle` suffixes. Example: `screen_onboarding_welcome_title`, `screen_change_server_subtitle`; -- `a11y_` pattern can be used for strings used for accessibility. Example: `a11y_hide_password`, `screen_roomlist_a11y_create_message`; +- `a11y_` pattern can be used for strings that are only used for accessibility. Example: `a11y_hide_password`, `screen_roomlist_a11y_create_message`; - Strings for error message can start by `error_`, or contain `_error_` if used in a specific screen only. Example: `error_some_messages_have_not_been_sent`, `screen_change_server_error_invalid_homeserver`. *Note*: those rules applies for `strings` and for `plurals`. From 85d24763d648da119ac35b96bcc18212a962e809 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Mar 2023 14:07:22 +0200 Subject: [PATCH 005/104] Add rule for string keys starting with `a11y`. --- tools/localazy/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/localazy/README.md b/tools/localazy/README.md index 6117cf17c07..10faf09ead8 100644 --- a/tools/localazy/README.md +++ b/tools/localazy/README.md @@ -23,6 +23,7 @@ Never edit manually the files `localazy.xml` or `translations.xml`!. For code clarity and in order to download strings to the correct module, here are some naming rules to follow as much as possible: - Keys for common strings, i.e. strings that can be used at multiple places must start by `action_` if this is a verb, or `common_` if not; +- Keys for common accessibility strings must start by `a11y_`. Example: `a11y_hide_password`; - Keys for strings used in a single screen must start with `screen_` followed by the screen name, followed by a free name. Example: `screen_onboarding_welcome_title`; - Keys can have `_title` or `_subtitle` suffixes. Example: `screen_onboarding_welcome_title`, `screen_change_server_subtitle`; - `a11y_` pattern can be used for strings that are only used for accessibility. Example: `a11y_hide_password`, `screen_roomlist_a11y_create_message`; From 17b52cb24246e68044593f661cedcd46175c5c0b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Mar 2023 14:09:46 +0200 Subject: [PATCH 006/104] typo --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 82602e7e632..125c69adb6b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ Please note that the Localazy project is not open yet for external contributions To help translating, please go to [https://localazy.com/p/element](https://localazy.com/p/element). -- If you want to fix an issue with an English string, please open an issue on the github project of ElementX (Android or iOS).Only the core team can modify or add English strings. +- If you want to fix an issue with an English string, please open an issue on the github project of ElementX (Android or iOS). Only the core team can modify or add English strings. - If you want to fix an issue in other languages, or add a missing translation, or even add a new language, please go to [https://localazy.com/p/element](https://localazy.com/p/element). More informations can be found [in this README.md](./tools/localazy/README.md). From 1019ede9f430b581dfe6f2fbee44f6071aad684c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Mar 2023 15:16:03 +0200 Subject: [PATCH 007/104] Add rules for platform suffixes. --- tools/localazy/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/localazy/README.md b/tools/localazy/README.md index 10faf09ead8..ed47efd2c32 100644 --- a/tools/localazy/README.md +++ b/tools/localazy/README.md @@ -6,6 +6,7 @@ Localazy is used to host the source strings and their translations. * [Localazy project](#localazy-project) * [Key naming rules](#key-naming-rules) + * [Special suffixes](#special-suffixes) * [CLI Installation](#cli-installation) * [Download translations](#download-translations) * [Add translations to a specific module](#add-translations-to-a-specific-module) @@ -31,6 +32,13 @@ For code clarity and in order to download strings to the correct module, here ar *Note*: those rules applies for `strings` and for `plurals`. +#### Special suffixes + +- if a key is suffixed by `_ios`, it will not be imported in the Android project; +- if a key is suffixed by `_android`, it will not be imported in the iOS project. + +So feel free to use those suffixes when necessary for instance when the string content is referring to something related to Android only, or iOS only. + ## CLI Installation To install the Localazy client, follow the instructions from [here](https://localazy.com/docs/cli/installation). From 515d6b451767cb48b69f455b3aab7acba52feefd Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 24 Mar 2023 16:01:14 +0100 Subject: [PATCH 008/104] Create or retrieve DM --- .../android/appnav/LoggedInFlowNode.kt | 12 ++++- features/createroom/api/build.gradle.kts | 1 + .../createroom/api/CreateRoomEntryPoint.kt | 19 +++++++- .../createroom/impl/CreateRoomFlowNode.kt | 7 +++ .../impl/DefaultCreateRoomEntryPoint.kt | 19 +++++++- .../impl/root/CreateRoomRootEvents.kt | 4 +- .../impl/root/CreateRoomRootNode.kt | 20 ++++---- .../impl/root/CreateRoomRootPresenter.kt | 46 +++++++++++++++++-- .../impl/root/CreateRoomRootState.kt | 4 ++ .../impl/root/CreateRoomRootStateProvider.kt | 5 +- .../impl/root/CreateRoomRootView.kt | 41 ++++++++++++++++- .../impl/root/CreateRoomRootPresenterTests.kt | 9 ++-- .../selectusers/api/SelectUsersEvents.kt | 1 + .../impl/DefaultSelectUsersPresenter.kt | 1 + .../libraries/matrix/api/MatrixClient.kt | 3 ++ .../libraries/matrix/impl/RustMatrixClient.kt | 27 +++++++++++ .../libraries/matrix/test/FakeMatrixClient.kt | 29 ++++++++++-- 17 files changed, 220 insertions(+), 28 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 4b2d653d792..fa2643b4ff6 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -32,6 +32,7 @@ import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.push +import com.bumble.appyx.navmodel.backstack.operation.replace import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode @@ -178,7 +179,16 @@ class LoggedInFlowNode @AssistedInject constructor( .build() } NavTarget.CreateRoom -> { - createRoomEntryPoint.createNode(this, buildContext) + val callback = object : CreateRoomEntryPoint.Callback { + override fun onOpenRoom(roomId: RoomId) { + backstack.replace(NavTarget.Room(roomId)) + } + } + + createRoomEntryPoint + .nodeBuilder(this, buildContext) + .callback(callback) + .build() } NavTarget.VerifySession -> { verifySessionEntryPoint.createNode(this, buildContext) diff --git a/features/createroom/api/build.gradle.kts b/features/createroom/api/build.gradle.kts index b3fceedc270..cafcf7c4b51 100644 --- a/features/createroom/api/build.gradle.kts +++ b/features/createroom/api/build.gradle.kts @@ -24,4 +24,5 @@ android { dependencies { implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) } diff --git a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt index 049c101806a..73f5110daa7 100644 --- a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt +++ b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt @@ -16,6 +16,21 @@ package io.element.android.features.createroom.api -import io.element.android.libraries.architecture.SimpleFeatureEntryPoint +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.matrix.api.core.RoomId -interface CreateRoomEntryPoint : SimpleFeatureEntryPoint +interface CreateRoomEntryPoint : FeatureEntryPoint { + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onOpenRoom(roomId: RoomId) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt index 13c3ff52bed..22851904c45 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt @@ -23,17 +23,20 @@ import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.createroom.impl.addpeople.AddPeopleNode import io.element.android.features.createroom.impl.root.CreateRoomRootNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) @@ -64,6 +67,10 @@ class CreateRoomFlowNode @AssistedInject constructor( override fun onCreateNewRoom() { backstack.push(NavTarget.NewRoom) } + + override fun onOpenRoom(roomId: RoomId) { + plugins().forEach { it.onOpenRoom(roomId) } + } } createNode(buildContext, plugins = listOf(callback)) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt index 214ed3a9ecb..34e514be3e5 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt @@ -18,6 +18,7 @@ package io.element.android.features.createroom.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.libraries.architecture.createNode @@ -26,7 +27,21 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultCreateRoomEntryPoint @Inject constructor() : CreateRoomEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreateRoomEntryPoint.NodeBuilder { + + val plugins = ArrayList() + + return object : CreateRoomEntryPoint.NodeBuilder { + + override fun callback(callback: CreateRoomEntryPoint.Callback): CreateRoomEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt index 5d2f0f684cc..d90708ed2b1 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt @@ -19,6 +19,8 @@ package io.element.android.features.createroom.impl.root import io.element.android.libraries.matrix.ui.model.MatrixUser sealed interface CreateRoomRootEvents { - data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents object InvitePeople : CreateRoomRootEvents + data class SelectUser(val matrixUser: MatrixUser) : CreateRoomRootEvents + data class CreateDM(val matrixUser: MatrixUser) : CreateRoomRootEvents + object CancelCreateDM : CreateRoomRootEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt index dadc16efc46..3a714a0eb33 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt @@ -16,7 +16,6 @@ package io.element.android.features.createroom.impl.root -import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext @@ -27,7 +26,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.SessionScope -import kotlinx.parcelize.Parcelize +import io.element.android.libraries.matrix.api.core.RoomId @ContributesNode(SessionScope::class) class CreateRoomRootNode @AssistedInject constructor( @@ -38,15 +37,17 @@ class CreateRoomRootNode @AssistedInject constructor( interface Callback : Plugin { fun onCreateNewRoom() + fun onOpenRoom(roomId: RoomId) } - private fun onCreateNewRoom() { - plugins().forEach { it.onCreateNewRoom() } - } + private val callback = object : Callback { + override fun onCreateNewRoom() { + plugins().forEach { it.onCreateNewRoom() } + } - sealed interface NavTarget : Parcelable { - @Parcelize - object Root : NavTarget + override fun onOpenRoom(roomId: RoomId) { + plugins().forEach { it.onOpenRoom(roomId) } + } } @Composable @@ -56,7 +57,8 @@ class CreateRoomRootNode @AssistedInject constructor( state = state, modifier = modifier, onClosePressed = this::navigateUp, - onNewRoomClicked = this::onCreateNewRoom, + onNewRoomClicked = callback::onCreateNewRoom, + onOpenDM = callback::onOpenRoom, ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 2f3f3ded4ad..68901037f0e 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -17,16 +17,30 @@ package io.element.android.features.createroom.impl.root import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.element.android.features.selectusers.api.SelectUsersEvents import io.element.android.features.selectusers.api.SelectUsersPresenter import io.element.android.features.selectusers.api.SelectUsersPresenterArgs import io.element.android.features.selectusers.api.SelectionMode +import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.execute +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.model.MatrixUser -import timber.log.Timber +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import javax.inject.Inject class CreateRoomRootPresenter @Inject constructor( private val presenterFactory: SelectUsersPresenter.Factory, + private val matrixClient: MatrixClient, ) : Presenter { private val presenter by lazy { @@ -37,20 +51,44 @@ class CreateRoomRootPresenter @Inject constructor( override fun present(): CreateRoomRootState { val selectUsersState = presenter.present() + val localCoroutineScope = rememberCoroutineScope() + + var showCreateDmConfirmationDialog by rememberSaveable { mutableStateOf(false) } + val startDmAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + fun handleEvents(event: CreateRoomRootEvents) { when (event) { - is CreateRoomRootEvents.StartDM -> handleStartDM(event.matrixUser) + is CreateRoomRootEvents.SelectUser -> { + val existingDM = matrixClient.findDM(event.matrixUser.id) + if (existingDM == null) { + showCreateDmConfirmationDialog = true + } else { + startDmAction.value = Async.Success(existingDM.roomId) + } + } + is CreateRoomRootEvents.CreateDM -> { + showCreateDmConfirmationDialog = false + localCoroutineScope.createDM(event.matrixUser, startDmAction) + } + CreateRoomRootEvents.CancelCreateDM -> { + showCreateDmConfirmationDialog = false + selectUsersState.eventSink(SelectUsersEvents.ClearSelection) + } CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action } } return CreateRoomRootState( selectUsersState = selectUsersState, + showCreateDmConfirmationDialog = showCreateDmConfirmationDialog, + startDmAction = startDmAction.value, eventSink = ::handleEvents, ) } - private fun handleStartDM(matrixUser: MatrixUser) { - Timber.d("handleStartDM: $matrixUser") // Todo handle start DM action + private fun CoroutineScope.createDM(user: MatrixUser, startDmAction: MutableState>) = launch { + suspend { + matrixClient.createDM(user.id).getOrThrow() + }.execute(startDmAction) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt index a57d6aaaf6a..89b702a011b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt @@ -17,8 +17,12 @@ package io.element.android.features.createroom.impl.root import io.element.android.features.selectusers.api.SelectUsersState +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.core.RoomId data class CreateRoomRootState( val selectUsersState: SelectUsersState, + val showCreateDmConfirmationDialog: Boolean, + val startDmAction: Async, val eventSink: (CreateRoomRootEvents) -> Unit, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index 678f02476c9..750b9cfe32b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -18,6 +18,7 @@ package io.element.android.features.createroom.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.selectusers.api.aSelectUsersState +import io.element.android.libraries.architecture.Async open class CreateRoomRootStateProvider : PreviewParameterProvider { override val values: Sequence @@ -28,5 +29,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider Unit = {}, onNewRoomClicked: () -> Unit = {}, + onOpenDM: (RoomId) -> Unit = {}, ) { + if (state.startDmAction is Async.Success) { + LaunchedEffect(state.startDmAction) { + onOpenDM(state.startDmAction.state) + } + } + Scaffold( modifier = modifier.fillMaxWidth(), topBar = { @@ -72,7 +85,7 @@ fun CreateRoomRootView( SelectUsersView( modifier = Modifier.fillMaxWidth(), state = state.selectUsersState, - onUserSelected = { state.eventSink.invoke(CreateRoomRootEvents.StartDM(it)) }, + onUserSelected = { state.eventSink(CreateRoomRootEvents.SelectUser(it)) }, ) if (!state.selectUsersState.isSearchActive) { @@ -83,6 +96,12 @@ fun CreateRoomRootView( } } } + + CreateDmConfirmationDialog(state) + + if (state.startDmAction is Async.Loading) { + ProgressDialog(text = "Creating room...") + } } @OptIn(ExperimentalMaterial3Api::class) @@ -153,6 +172,26 @@ fun CreateRoomActionButton( } } +@Composable +fun CreateDmConfirmationDialog( + state: CreateRoomRootState, + modifier: Modifier = Modifier, +) { + if (state.showCreateDmConfirmationDialog) { + val selectedUser = state.selectUsersState.selectedUsers.firstOrNull() + if (selectedUser != null) { + ConfirmationDialog( + modifier = modifier, + title = "Start chat", + content = "You're about starting a chat with ${selectedUser.getBestName()}, do you want to continue?", + submitText = stringResource(io.element.android.libraries.ui.strings.R.string._continue), + onSubmitClicked = { state.eventSink(CreateRoomRootEvents.CreateDM(selectedUser)) }, + onDismiss = { state.eventSink(CreateRoomRootEvents.CancelCreateDM) }, + ) + } + } +} + @Preview @Composable fun CreateRoomRootViewLightPreview(@PreviewParameter(CreateRoomRootStateProvider::class) state: CreateRoomRootState) = diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index 5dc9ec00e91..4aa1068ce4d 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -25,6 +25,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.selectusers.api.SelectUsersPresenterArgs import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -34,13 +35,15 @@ import org.junit.Test class CreateRoomRootPresenterTests { private lateinit var presenter: CreateRoomRootPresenter + private lateinit var fakeMatrixClient: FakeMatrixClient @Before fun setup() { val selectUsersPresenter = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory { override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args) } - presenter = CreateRoomRootPresenter(selectUsersPresenter) + fakeMatrixClient = FakeMatrixClient() + presenter = CreateRoomRootPresenter(selectUsersPresenter, fakeMatrixClient) } @Test @@ -64,13 +67,13 @@ class CreateRoomRootPresenterTests { } @Test - fun `present - trigger start DM action`() = runTest { + fun `present - trigger select user action`() = runTest { moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() val matrixUser = MatrixUser(UserId("@name:matrix.org")) - initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + initialState.eventSink(CreateRoomRootEvents.SelectUser(matrixUser)) } } } diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt index e0ee6ddf68f..22c72f6dfb1 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt +++ b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt @@ -22,5 +22,6 @@ sealed interface SelectUsersEvents { data class UpdateSearchQuery(val query: String) : SelectUsersEvents data class AddToSelection(val matrixUser: MatrixUser) : SelectUsersEvents data class RemoveFromSelection(val matrixUser: MatrixUser) : SelectUsersEvents + object ClearSelection : SelectUsersEvents data class OnSearchActiveChanged(val active: Boolean) : SelectUsersEvents } diff --git a/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt b/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt index e1135cd1a2c..55c2cd694d3 100644 --- a/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt +++ b/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt @@ -79,6 +79,7 @@ class DefaultSelectUsersPresenter @AssistedInject constructor( localCoroutineScope.scrollToFirstSelectedUser(selectedUsersListState) } is SelectUsersEvents.RemoveFromSelection -> selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList() + SelectUsersEvents.ClearSelection -> selectedUsers.value = persistentListOf() } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 9ce55028439..15ba838bcbf 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.api import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaResolver import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource @@ -28,6 +29,8 @@ interface MatrixClient : Closeable { val sessionId: SessionId val roomSummaryDataSource: RoomSummaryDataSource fun getRoom(roomId: RoomId): MatrixRoom? + suspend fun createDM(userId: UserId): Result + fun findDM(userId: UserId): MatrixRoom? fun startSync() fun stopSync() fun mediaResolver(): MediaResolver diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 378fec56c72..ce9286d2bec 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -37,7 +37,10 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientDelegate +import org.matrix.rustcomponents.sdk.CreateRoomParameters import org.matrix.rustcomponents.sdk.RequiredState +import org.matrix.rustcomponents.sdk.RoomPreset +import org.matrix.rustcomponents.sdk.RoomVisibility import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder import org.matrix.rustcomponents.sdk.SlidingSyncMode import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters @@ -154,6 +157,30 @@ class RustMatrixClient constructor( ) } + override fun findDM(userId: UserId): MatrixRoom? { + val roomId = client.getDmRoom(userId.value)?.use { RoomId(it.id()) } + return roomId?.let { getRoom(it) } + } + + override suspend fun createDM(userId: UserId): Result = + withContext(dispatchers.io) { + runCatching { + val roomId = client.createRoom( + CreateRoomParameters( + name = "", + topic = null, + isEncrypted = true, + isDirect = true, + visibility = RoomVisibility.PRIVATE, + preset = RoomPreset.TRUSTED_PRIVATE_CHAT, + invite = listOf(userId.value), + avatar = null, + ) + ) + RoomId(roomId) + } + } + override fun mediaResolver(): MediaResolver = mediaResolver override fun sessionVerificationService(): SessionVerificationService = verificationService diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 9272e794b4a..ec5068e93d6 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.test import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaResolver import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource @@ -37,12 +38,22 @@ class FakeMatrixClient( private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService() ) : MatrixClient { + private var createDmResult: Result = Result.success(A_ROOM_ID) + private var findDmResult: MatrixRoom? = FakeMatrixRoom() private var logoutFailure: Throwable? = null override fun getRoom(roomId: RoomId): MatrixRoom? { return FakeMatrixRoom(roomId) } + override suspend fun createDM(userId: UserId): Result { + return createDmResult + } + + override fun findDM(userId: UserId): MatrixRoom? { + return findDmResult + } + override fun startSync() = Unit override fun stopSync() = Unit @@ -51,10 +62,6 @@ class FakeMatrixClient( return FakeMediaResolver() } - fun givenLogoutError(failure: Throwable) { - logoutFailure = failure - } - override suspend fun logout() { delay(100) logoutFailure?.let { throw it } @@ -81,4 +88,18 @@ class FakeMatrixClient( override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService override fun onSlidingSyncUpdate() {} + + // Mocks + + fun givenLogoutError(failure: Throwable) { + logoutFailure = failure + } + + fun givenCreateDmResult(result: Result) { + createDmResult = result + } + + fun givenFindDmResult(result: MatrixRoom?) { + findDmResult = result + } } From 5a3b30a0e42705cf6ced66734fe39fefb1670fc6 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 07:41:04 +0200 Subject: [PATCH 009/104] convert rustsdk gradle file to kts --- libraries/rustsdk/build.gradle | 2 -- libraries/rustsdk/build.gradle.kts | 2 ++ settings.gradle.kts | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 libraries/rustsdk/build.gradle create mode 100644 libraries/rustsdk/build.gradle.kts diff --git a/libraries/rustsdk/build.gradle b/libraries/rustsdk/build.gradle deleted file mode 100644 index bfafe67f282..00000000000 --- a/libraries/rustsdk/build.gradle +++ /dev/null @@ -1,2 +0,0 @@ -configurations.maybeCreate("default") -artifacts.add("default", file('matrix-rust-sdk.aar')) \ No newline at end of file diff --git a/libraries/rustsdk/build.gradle.kts b/libraries/rustsdk/build.gradle.kts new file mode 100644 index 00000000000..56fd0948970 --- /dev/null +++ b/libraries/rustsdk/build.gradle.kts @@ -0,0 +1,2 @@ +configurations.maybeCreate("default") +artifacts.add("default", file("matrix-rust-sdk.aar")) diff --git a/settings.gradle.kts b/settings.gradle.kts index 944b17ab523..e44a2cb8fa0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,7 +41,6 @@ include(":appnav") include(":tests:uitests") include(":anvilannotations") include(":anvilcodegen") -include(":libraries:rustsdk") include(":samples:minimal") From bb5727aad5f3bff530ff16d34a7fa593d3e50cf6 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 07:41:44 +0200 Subject: [PATCH 010/104] fix rebase issue --- .../android/features/createroom/impl/root/CreateRoomRootView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 8c0e9382d05..6259840a216 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -184,7 +184,7 @@ fun CreateDmConfirmationDialog( modifier = modifier, title = "Start chat", content = "You're about starting a chat with ${selectedUser.getBestName()}, do you want to continue?", - submitText = stringResource(io.element.android.libraries.ui.strings.R.string._continue), + submitText = stringResource(io.element.android.libraries.ui.strings.R.string.action_continue), onSubmitClicked = { state.eventSink(CreateRoomRootEvents.CreateDM(selectedUser)) }, onDismiss = { state.eventSink(CreateRoomRootEvents.CancelCreateDM) }, ) From 8ff1752bcfebf17b94884fcefb51e9b555bb8a3a Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 08:47:06 +0200 Subject: [PATCH 011/104] Add preview --- .../impl/root/CreateRoomRootStateProvider.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index 750b9cfe32b..be720de5b2f 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -19,11 +19,24 @@ package io.element.android.features.createroom.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.selectusers.api.aSelectUsersState import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import kotlinx.collections.immutable.persistentListOf open class CreateRoomRootStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aCreateRoomRootState(), + aCreateRoomRootState().copy( + showCreateDmConfirmationDialog = true, + selectUsersState = aMatrixUser().let { + aSelectUsersState().copy( + searchQuery = it.id.value, + searchResults = persistentListOf(it), + selectedUsers = persistentListOf(it), + isSearchActive = true, + ) + } + ), ) } From 18a08885c493ca152e385385ef072267558a65dc Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 09:34:13 +0200 Subject: [PATCH 012/104] Remove confirmation dialog --- .../impl/root/CreateRoomRootEvents.kt | 4 +-- .../impl/root/CreateRoomRootPresenter.kt | 19 ++------------ .../impl/root/CreateRoomRootState.kt | 1 - .../impl/root/CreateRoomRootStateProvider.kt | 5 ++-- .../impl/root/CreateRoomRootView.kt | 26 +------------------ .../impl/root/CreateRoomRootPresenterTests.kt | 2 +- .../selectusers/api/SelectUsersEvents.kt | 1 - .../impl/DefaultSelectUsersPresenter.kt | 1 - 8 files changed, 7 insertions(+), 52 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt index d90708ed2b1..d3cd1e02878 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt @@ -20,7 +20,5 @@ import io.element.android.libraries.matrix.ui.model.MatrixUser sealed interface CreateRoomRootEvents { object InvitePeople : CreateRoomRootEvents - data class SelectUser(val matrixUser: MatrixUser) : CreateRoomRootEvents - data class CreateDM(val matrixUser: MatrixUser) : CreateRoomRootEvents - object CancelCreateDM : CreateRoomRootEvents + data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 68901037f0e..2ec32c7bac5 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -18,13 +18,9 @@ package io.element.android.features.createroom.impl.root import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import io.element.android.features.selectusers.api.SelectUsersEvents import io.element.android.features.selectusers.api.SelectUsersPresenter import io.element.android.features.selectusers.api.SelectUsersPresenterArgs import io.element.android.features.selectusers.api.SelectionMode @@ -52,35 +48,24 @@ class CreateRoomRootPresenter @Inject constructor( val selectUsersState = presenter.present() val localCoroutineScope = rememberCoroutineScope() - - var showCreateDmConfirmationDialog by rememberSaveable { mutableStateOf(false) } val startDmAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } fun handleEvents(event: CreateRoomRootEvents) { when (event) { - is CreateRoomRootEvents.SelectUser -> { + is CreateRoomRootEvents.StartDM -> { val existingDM = matrixClient.findDM(event.matrixUser.id) if (existingDM == null) { - showCreateDmConfirmationDialog = true + localCoroutineScope.createDM(event.matrixUser, startDmAction) } else { startDmAction.value = Async.Success(existingDM.roomId) } } - is CreateRoomRootEvents.CreateDM -> { - showCreateDmConfirmationDialog = false - localCoroutineScope.createDM(event.matrixUser, startDmAction) - } - CreateRoomRootEvents.CancelCreateDM -> { - showCreateDmConfirmationDialog = false - selectUsersState.eventSink(SelectUsersEvents.ClearSelection) - } CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action } } return CreateRoomRootState( selectUsersState = selectUsersState, - showCreateDmConfirmationDialog = showCreateDmConfirmationDialog, startDmAction = startDmAction.value, eventSink = ::handleEvents, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt index 89b702a011b..9b8b81aef19 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt @@ -22,7 +22,6 @@ import io.element.android.libraries.matrix.api.core.RoomId data class CreateRoomRootState( val selectUsersState: SelectUsersState, - val showCreateDmConfirmationDialog: Boolean, val startDmAction: Async, val eventSink: (CreateRoomRootEvents) -> Unit, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index be720de5b2f..e0e31523185 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -27,7 +27,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList() - SelectUsersEvents.ClearSelection -> selectedUsers.value = persistentListOf() } } From 0eaee7513ede58d8999df12d31b52183a331ade7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 10:01:04 +0200 Subject: [PATCH 013/104] Add presenter tests --- .../impl/root/CreateRoomRootPresenterTests.kt | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index 1cca23f74ac..c3cacb25c7e 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -24,8 +24,11 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.selectusers.api.SelectUsersPresenterArgs import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -67,13 +70,39 @@ class CreateRoomRootPresenterTests { } @Test - fun `present - trigger select user action`() = runTest { + fun `present - trigger create DM action`() = runTest { moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() val matrixUser = MatrixUser(UserId("@name:matrix.org")) + val createDmResult = Result.success(RoomId("!createDmResult")) + + fakeMatrixClient.givenFindDmResult(null) + fakeMatrixClient.givenCreateDmResult(createDmResult) + + initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + val stateAfterStartDM = awaitItem() + assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Success::class.java) + assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull()) + } + } + + @Test + fun `present - trigger retrieve DM action`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val matrixUser = MatrixUser(UserId("@name:matrix.org")) + val fakeDmResult = FakeMatrixRoom(RoomId("!fakeDmResult")) + + fakeMatrixClient.givenFindDmResult(fakeDmResult) + initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + val stateAfterStartDM = awaitItem() + assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Success::class.java) + assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(fakeDmResult.roomId) } } } From c72980d0941bbac265f2efa959d95b128fa92d41 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 10:13:36 +0200 Subject: [PATCH 014/104] Use string resource --- .../android/features/createroom/impl/root/CreateRoomRootView.kt | 2 +- libraries/ui-strings/src/main/res/values/localazy.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index b75bddc8e01..4266878a33a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -96,7 +96,7 @@ fun CreateRoomRootView( } if (state.startDmAction is Async.Loading) { - ProgressDialog(text = "Creating room...") + ProgressDialog(text = stringResource(id = StringR.string.common_creating_room)) } } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index a28d8bb7bc5..084a442442d 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -48,6 +48,7 @@ "About" "Audio" "Bubbles" + "Creating room…" "Decryption error" "Developer options" "(edited)" From e63a761c94df92c5dd147c72dff9ad395470e0c6 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 10:51:39 +0200 Subject: [PATCH 015/104] Changelog --- changelog.d/96.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/96.feature diff --git a/changelog.d/96.feature b/changelog.d/96.feature new file mode 100644 index 00000000000..7a1c8d21b89 --- /dev/null +++ b/changelog.d/96.feature @@ -0,0 +1 @@ +[Create and join rooms] Show or create direct message room From 7e6a466386976714177bf55809c47c2a87a22f09 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 13:48:54 +0200 Subject: [PATCH 016/104] Handle errors on create DM --- .../impl/root/CreateRoomRootEvents.kt | 2 + .../impl/root/CreateRoomRootPresenter.kt | 22 ++++-- .../impl/root/CreateRoomRootStateProvider.kt | 13 +++- .../impl/root/CreateRoomRootView.kt | 17 ++++- .../impl/root/CreateRoomRootPresenterTests.kt | 72 +++++++++++++++++-- .../components/dialogs/ErrorDialog.kt | 11 ++- .../libraries/matrix/test/FakeMatrixClient.kt | 9 ++- 7 files changed, 130 insertions(+), 16 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt index d3cd1e02878..de9810f34ad 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt @@ -21,4 +21,6 @@ import io.element.android.libraries.matrix.ui.model.MatrixUser sealed interface CreateRoomRootEvents { object InvitePeople : CreateRoomRootEvents data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents + object RetryStartDM : CreateRoomRootEvents + object CancelStartDM : CreateRoomRootEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 2ec32c7bac5..6bc8d3c45e3 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -50,16 +50,24 @@ class CreateRoomRootPresenter @Inject constructor( val localCoroutineScope = rememberCoroutineScope() val startDmAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + fun startDm(matrixUser: MatrixUser) { + startDmAction.value = Async.Uninitialized + val existingDM = matrixClient.findDM(matrixUser.id) + if (existingDM == null) { + localCoroutineScope.createDM(matrixUser, startDmAction) + } else { + startDmAction.value = Async.Success(existingDM.roomId) + } + } + fun handleEvents(event: CreateRoomRootEvents) { when (event) { - is CreateRoomRootEvents.StartDM -> { - val existingDM = matrixClient.findDM(event.matrixUser.id) - if (existingDM == null) { - localCoroutineScope.createDM(event.matrixUser, startDmAction) - } else { - startDmAction.value = Async.Success(existingDM.roomId) - } + is CreateRoomRootEvents.StartDM -> startDm(event.matrixUser) + CreateRoomRootEvents.RetryStartDM -> { + startDmAction.value = Async.Uninitialized + selectUsersState.selectedUsers.firstOrNull()?.let { startDm(it) } } + CreateRoomRootEvents.CancelStartDM -> startDmAction.value = Async.Uninitialized CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index e0e31523185..e8f739c738b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -36,7 +36,18 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider { + ProgressDialog(text = stringResource(id = StringR.string.common_creating_room)) + } + is Async.Failure -> { + ErrorDialog( + content = stringResource(id = StringR.string.screen_start_chat_error_starting_chat), + dismissText = stringResource(id = StringR.string.action_cancel), + submitText = stringResource(id = StringR.string.action_retry), + onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) }, + onSubmit = { state.eventSink(CreateRoomRootEvents.RetryStartDM) }, + ) + } + else -> Unit } } diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index c3cacb25c7e..6853fed3d8f 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -18,18 +18,23 @@ package io.element.android.features.createroom.impl.root +import androidx.compose.runtime.Composable import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.selectusers.api.SelectUsersPresenter import io.element.android.features.selectusers.api.SelectUsersPresenterArgs -import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter +import io.element.android.features.selectusers.api.SelectUsersState +import io.element.android.features.selectusers.api.aSelectUsersState import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before @@ -38,15 +43,17 @@ import org.junit.Test class CreateRoomRootPresenterTests { private lateinit var presenter: CreateRoomRootPresenter + private lateinit var fakeSelectUsersPresenter: FakeSelectUserPresenter private lateinit var fakeMatrixClient: FakeMatrixClient @Before fun setup() { - val selectUsersPresenter = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory { - override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args) + val factory = object : SelectUsersPresenter.Factory { + override fun create(args: SelectUsersPresenterArgs) = fakeSelectUsersPresenter } + fakeSelectUsersPresenter = FakeSelectUserPresenter() fakeMatrixClient = FakeMatrixClient() - presenter = CreateRoomRootPresenter(selectUsersPresenter, fakeMatrixClient) + presenter = CreateRoomRootPresenter(factory, fakeMatrixClient) } @Test @@ -82,6 +89,7 @@ class CreateRoomRootPresenterTests { fakeMatrixClient.givenCreateDmResult(createDmResult) initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) val stateAfterStartDM = awaitItem() assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Success::class.java) assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull()) @@ -105,4 +113,60 @@ class CreateRoomRootPresenterTests { assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(fakeDmResult.roomId) } } + + @Test + fun `present - trigger retry create DM action`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val matrixUser = MatrixUser(UserId("@name:matrix.org")) + val createDmResult = Result.success(RoomId("!createDmResult")) + fakeSelectUsersPresenter.givenState(aSelectUsersState().copy(selectedUsers = persistentListOf(matrixUser))) + + fakeMatrixClient.givenFindDmResult(null) + fakeMatrixClient.givenCreateDmError(A_THROWABLE) + fakeMatrixClient.givenCreateDmResult(createDmResult) + + // Failure + initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) + val stateAfterStartDM = awaitItem() + assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Failure::class.java) + + // Cancel + stateAfterStartDM.eventSink(CreateRoomRootEvents.CancelStartDM) + val stateAfterCancel = awaitItem() + assertThat(stateAfterCancel.startDmAction).isInstanceOf(Async.Uninitialized::class.java) + + // Failure + stateAfterCancel.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) + val stateAfterSecondAttempt = awaitItem() + assertThat(stateAfterSecondAttempt.startDmAction).isInstanceOf(Async.Failure::class.java) + + // Retry with success + fakeMatrixClient.givenCreateDmError(null) + stateAfterSecondAttempt.eventSink(CreateRoomRootEvents.RetryStartDM) + assertThat(awaitItem().startDmAction).isInstanceOf(Async.Uninitialized::class.java) + assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) + val stateAfterRetryStartDM = awaitItem() + assertThat(stateAfterRetryStartDM.startDmAction).isInstanceOf(Async.Success::class.java) + assertThat(stateAfterRetryStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull()) + } + } + + private class FakeSelectUserPresenter : SelectUsersPresenter { + + private var state = aSelectUsersState() + + fun givenState(state: SelectUsersState) { + this.state = state + } + + @Composable + override fun present(): SelectUsersState { + return state + } + } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt index ce1730ed0c3..da1aeb836ec 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt @@ -37,7 +37,9 @@ fun ErrorDialog( modifier: Modifier = Modifier, title: String = ErrorDialogDefaults.title, submitText: String = ErrorDialogDefaults.submitText, + dismissText: String? = null, onDismiss: () -> Unit = {}, + onSubmit: () -> Unit = onDismiss, shape: Shape = AlertDialogDefaults.shape, containerColor: Color = AlertDialogDefaults.containerColor, iconContentColor: Color = AlertDialogDefaults.iconContentColor, @@ -55,10 +57,17 @@ fun ErrorDialog( Text(content) }, confirmButton = { - TextButton(onClick = onDismiss) { + TextButton(onClick = onSubmit) { Text(submitText) } }, + dismissButton = dismissText?.let { + { + TextButton(onClick = onDismiss) { + Text(it) + } + } + }, shape = shape, containerColor = containerColor, iconContentColor = iconContentColor, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index ec5068e93d6..5889d35e0c3 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -39,6 +39,7 @@ class FakeMatrixClient( ) : MatrixClient { private var createDmResult: Result = Result.success(A_ROOM_ID) + private var createDmFailure: Throwable? = null private var findDmResult: MatrixRoom? = FakeMatrixRoom() private var logoutFailure: Throwable? = null @@ -47,6 +48,8 @@ class FakeMatrixClient( } override suspend fun createDM(userId: UserId): Result { + delay(100) + createDmFailure?.let { throw it } return createDmResult } @@ -91,7 +94,7 @@ class FakeMatrixClient( // Mocks - fun givenLogoutError(failure: Throwable) { + fun givenLogoutError(failure: Throwable?) { logoutFailure = failure } @@ -99,6 +102,10 @@ class FakeMatrixClient( createDmResult = result } + fun givenCreateDmError(failure: Throwable?) { + createDmFailure = failure + } + fun givenFindDmResult(result: MatrixRoom?) { findDmResult = result } From c457d7fe5f53a14c85d077281d047ab8f904a4cf Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 16:18:15 +0200 Subject: [PATCH 017/104] Add new screenshots --- ..._CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ..._CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ 4 files changed, 12 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 00000000000..1e92cf978d0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f29da5d5aeb65659b065b7bd6afe276f83e020545a027780d2391308d1a4076 +size 20750 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 00000000000..74acb423d66 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7b91a2d05b975d568116615c286568f376ebead49e25ff17f5aae8b75be0e1f +size 28876 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 00000000000..23c4de31941 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae3e8c4e952b97628d026dfe78781aef894d6c2e742ac6ae1f1a2c0170df159e +size 20382 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 00000000000..bb7d1036e41 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44a35f28b3a59cc28937fee16eda26c26ef7b7622f929218e40f7537e096b2e8 +size 28120 From a957d9fd6a4fdd9a77530f3e172156bbcac8fea7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 30 Mar 2023 08:49:48 +0200 Subject: [PATCH 018/104] Pass null name when creating DM --- .../element/android/libraries/matrix/impl/RustMatrixClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index ce9286d2bec..3dc630c42db 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -167,7 +167,7 @@ class RustMatrixClient constructor( runCatching { val roomId = client.createRoom( CreateRoomParameters( - name = "", + name = null, topic = null, isEncrypted = true, isDirect = true, From e6edeb49e8d0344ba57fb9ad727e43a18cb514a0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 3 Apr 2023 16:27:34 +0200 Subject: [PATCH 019/104] Move FakeSelectUserPresenter to dedicated module --- features/createroom/impl/build.gradle.kts | 1 + .../impl/addpeople/AddPeoplePresenterTests.kt | 8 ++--- .../impl/root/CreateRoomRootPresenterTests.kt | 17 +-------- features/selectusers/test/build.gradle.kts | 28 +++++++++++++++ .../test/FakeSelectUserPresenter.kt | 36 +++++++++++++++++++ .../test/FakeSelectUserPresenterFactory.kt | 25 +++++++++++++ 6 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 features/selectusers/test/build.gradle.kts create mode 100644 features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenter.kt create mode 100644 features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenterFactory.kt diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 366cc1e0bdf..b68ad614e25 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.selectusers.impl) + testImplementation(projects.features.selectusers.test) androidTestImplementation(libs.test.junitext) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt index d9f40c1dbf0..caf1544a8f4 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt @@ -22,8 +22,7 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.selectusers.api.SelectUsersPresenterArgs -import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter +import io.element.android.features.selectusers.test.FakeSelectUserPresenterFactory import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before @@ -35,10 +34,7 @@ class AddPeoplePresenterTests { @Before fun setup() { - val selectUsersFactory = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory { - override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args) - } - presenter = AddPeoplePresenter(selectUsersFactory) + presenter = AddPeoplePresenter(FakeSelectUserPresenterFactory()) } @Test diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index 6853fed3d8f..28f46680332 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -18,15 +18,14 @@ package io.element.android.features.createroom.impl.root -import androidx.compose.runtime.Composable import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.selectusers.api.SelectUsersPresenter import io.element.android.features.selectusers.api.SelectUsersPresenterArgs -import io.element.android.features.selectusers.api.SelectUsersState import io.element.android.features.selectusers.api.aSelectUsersState +import io.element.android.features.selectusers.test.FakeSelectUserPresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -155,18 +154,4 @@ class CreateRoomRootPresenterTests { assertThat(stateAfterRetryStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull()) } } - - private class FakeSelectUserPresenter : SelectUsersPresenter { - - private var state = aSelectUsersState() - - fun givenState(state: SelectUsersState) { - this.state = state - } - - @Composable - override fun present(): SelectUsersState { - return state - } - } } diff --git a/features/selectusers/test/build.gradle.kts b/features/selectusers/test/build.gradle.kts new file mode 100644 index 00000000000..fb4e5057094 --- /dev/null +++ b/features/selectusers/test/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 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. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.features.selectusers.test" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.features.selectusers.api) +} diff --git a/features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenter.kt b/features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenter.kt new file mode 100644 index 00000000000..e92c99df920 --- /dev/null +++ b/features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenter.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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 io.element.android.features.selectusers.test + +import androidx.compose.runtime.Composable +import io.element.android.features.selectusers.api.SelectUsersPresenter +import io.element.android.features.selectusers.api.SelectUsersState +import io.element.android.features.selectusers.api.aSelectUsersState + +class FakeSelectUserPresenter : SelectUsersPresenter { + + private var state = aSelectUsersState() + + fun givenState(state: SelectUsersState) { + this.state = state + } + + @Composable + override fun present(): SelectUsersState { + return state + } +} diff --git a/features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenterFactory.kt b/features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenterFactory.kt new file mode 100644 index 00000000000..7bf74370c9d --- /dev/null +++ b/features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenterFactory.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 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 io.element.android.features.selectusers.test + +import io.element.android.features.selectusers.api.SelectUsersPresenter +import io.element.android.features.selectusers.api.SelectUsersPresenterArgs + +class FakeSelectUserPresenterFactory : SelectUsersPresenter.Factory { + + override fun create(args: SelectUsersPresenterArgs): SelectUsersPresenter = FakeSelectUserPresenter() +} From e816d29f55410cf4172784637f0f9bdca081609f Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 3 Apr 2023 16:41:09 +0200 Subject: [PATCH 020/104] Unplug DM creation --- .../createroom/impl/root/CreateRoomRootView.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 63f0216ec98..fb211febc2a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -16,6 +16,7 @@ package io.element.android.features.createroom.impl.root +import android.widget.Toast import androidx.annotation.DrawableRes import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -32,6 +33,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -81,10 +83,16 @@ fun CreateRoomRootView( modifier = Modifier.padding(paddingValues), verticalArrangement = Arrangement.spacedBy(8.dp), ) { + val context = LocalContext.current SelectUsersView( modifier = Modifier.fillMaxWidth(), state = state.selectUsersState, - onUserSelected = { state.eventSink(CreateRoomRootEvents.StartDM(it)) }, + onUserSelected = { + // Fixme disabled DM creation since it can break the account data which is not correctly synced + // uncomment to enable it again or move behind a feature flag + Toast.makeText(context, "Create DM feature is disabled.", Toast.LENGTH_SHORT).show() +// state.eventSink(CreateRoomRootEvents.StartDM(it)) + }, ) if (!state.selectUsersState.isSearchActive) { From c9fe2b122f9932f64253c1c0c3f2e3c45ce2f2ae Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 4 Apr 2023 16:21:10 +0200 Subject: [PATCH 021/104] Remove hardcoded string --- .../createroom/impl/root/CreateRoomRootStateProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index e8f739c738b..85d9cb41c5f 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -38,7 +38,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider Date: Wed, 5 Apr 2023 10:58:18 +0200 Subject: [PATCH 022/104] Add ability to download only English string (for developer). This is default behavior. --- tools/localazy/README.md | 8 +++- tools/localazy/downloadStrings.sh | 18 +++++++-- tools/localazy/generateLocalazyConfig.py | 48 +++++++++++++----------- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/tools/localazy/README.md b/tools/localazy/README.md index dcb45c5be7a..968d92047da 100644 --- a/tools/localazy/README.md +++ b/tools/localazy/README.md @@ -20,7 +20,13 @@ In the root folder of the project, run: ./tools/localazy/downloadStrings.sh ``` -It will update all the `localazy.xml` and `translations.xml` resource files. In case of merge conflicts, just erase the files and download again using the script. +It will update all the `localazy.xml` resource files. In case of merge conflicts, just erase the files and download again using the script. + +To also include the translations, i.e. the `translations.xml` files, add `--all` argument: + +```shell +./tools/localazy/downloadStrings.sh --all +``` ## Add translations to a specific module diff --git a/tools/localazy/downloadStrings.sh b/tools/localazy/downloadStrings.sh index 163aab8e315..5892a87c388 100755 --- a/tools/localazy/downloadStrings.sh +++ b/tools/localazy/downloadStrings.sh @@ -18,12 +18,24 @@ set -e +if [[ $1 == "--all" ]]; then + echo "Note: I will update all the files." + allFiles=1 +else + echo "Note: I will update only the English files." + allFiles=0 +fi + echo "Generating the configuration file for localazy..." -./tools/localazy/generateLocalazyConfig.py +./tools/localazy/generateLocalazyConfig.py $allFiles -echo "Deleting all existing localazy.xml and translations.xml files..." +echo "Deleting all existing localazy.xml files..." find . -name 'localazy.xml' -delete -find . -name 'translations.xml' -delete + +if [[ $allFiles == 1 ]]; then + echo "Deleting all existing translations.xml files..." + find . -name 'translations.xml' -delete +fi echo "Importing the strings..." localazy download --config ./tools/localazy/localazy.json diff --git a/tools/localazy/generateLocalazyConfig.py b/tools/localazy/generateLocalazyConfig.py index 829541462e2..13b76e7d57c 100755 --- a/tools/localazy/generateLocalazyConfig.py +++ b/tools/localazy/generateLocalazyConfig.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 import json +import sys # Read the config.json file with open('./tools/localazy/config.json', 'r') as f: config = json.load(f) +allFiles = sys.argv[1] == "1" # Convert a module name to a path # Ex: ":features:verifysession:impl" => "features/verifysession/impl" @@ -35,20 +37,21 @@ def convertModuleToPath(name): "equals: ${languageCode}, en" ] } - # Create action for the translations - actionTranslation = { - "type": "android", - "output": convertModuleToPath(entry["name"]) + "/src/main/res/values-${langAndroidResNoScript}/translations.xml", - "includeKeys": list(map(lambda i: "REGEX:" + i, entry["includeRegex"])), - "excludeKeys": list(map(lambda i: "REGEX:" + i, regexToAlwaysExclude)), - "conditions": [ - "!equals: ${languageCode}, en" - ] - } # print(action) - allRegexToExcludeFromMainModule.extend(entry["includeRegex"]) allActions.append(action) - allActions.append(actionTranslation) + # Create action for the translations + if allFiles: + actionTranslation = { + "type": "android", + "output": convertModuleToPath(entry["name"]) + "/src/main/res/values-${langAndroidResNoScript}/translations.xml", + "includeKeys": list(map(lambda i: "REGEX:" + i, entry["includeRegex"])), + "excludeKeys": list(map(lambda i: "REGEX:" + i, regexToAlwaysExclude)), + "conditions": [ + "!equals: ${languageCode}, en" + ] + } + allActions.append(actionTranslation) + allRegexToExcludeFromMainModule.extend(entry["includeRegex"]) # Append configuration for the main string module: default language mainAction = { @@ -62,16 +65,17 @@ def convertModuleToPath(name): # print(mainAction) allActions.append(mainAction) -# Append configuration for the main string module: translations -mainActionTranslation = { - "type": "android", - "output": "libraries/ui-strings/src/main/res/values-${langAndroidResNoScript}/translations.xml", - "excludeKeys": list(map(lambda i: "REGEX:" + i, allRegexToExcludeFromMainModule + regexToAlwaysExclude)), - "conditions": [ - "!equals: ${languageCode}, en" - ] -} -allActions.append(mainActionTranslation) +if allFiles: + # Append configuration for the main string module: translations + mainActionTranslation = { + "type": "android", + "output": "libraries/ui-strings/src/main/res/values-${langAndroidResNoScript}/translations.xml", + "excludeKeys": list(map(lambda i: "REGEX:" + i, allRegexToExcludeFromMainModule + regexToAlwaysExclude)), + "conditions": [ + "!equals: ${languageCode}, en" + ] + } + allActions.append(mainActionTranslation) # Generate the configuration for localazy result = { From 1d9fda863149cc7f2c70a3308c73655d821c8b28 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 11:04:18 +0200 Subject: [PATCH 023/104] Add GitHub action to sync Localazy strings. --- .github/workflows/sync-localazy.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/sync-localazy.yml diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml new file mode 100644 index 00000000000..3efbb41f274 --- /dev/null +++ b/.github/workflows/sync-localazy.yml @@ -0,0 +1,28 @@ +name: Sync Localazy +on: + schedule: + # At 00:00 on every Monday UTC + - cron: '0 0 * * 1' + +jobs: + sync-localazy: + runs-on: ubuntu-latest + # Skip in forks + if: github.repository == 'vector-im/element-x-android' + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.8 + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Run Localazy script + run: ./tools/localazy/downloadStrings.sh --all + - name: Create Pull Request for Strings + uses: peter-evans/create-pull-request@v4 + with: + commit-message: Sync Strings from Localazy + title: Sync Strings + body: | + - Update Strings from Localazy + branch: sync-localazy + base: develop From da55cedb5c1044c2e6973db20ffb7c1efcf9089d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 11:11:47 +0200 Subject: [PATCH 024/104] Invoke `./tools/localazy/downloadStrings.sh --all` --- .../src/main/res/values-es/translations.xml | 6 + .../src/main/res/values-it/translations.xml | 6 + .../src/main/res/values-ro/translations.xml | 6 + .../src/main/res/values-es/translations.xml | 20 +++ .../src/main/res/values-it/translations.xml | 20 +++ .../src/main/res/values-ro/translations.xml | 20 +++ .../src/main/res/values-es/translations.xml | 8 + .../src/main/res/values-it/translations.xml | 8 + .../src/main/res/values-ro/translations.xml | 8 + .../src/main/res/values-es/translations.xml | 5 + .../src/main/res/values-it/translations.xml | 5 + .../src/main/res/values-ro/translations.xml | 5 + .../src/main/res/values-es/translations.xml | 5 + .../src/main/res/values-it/translations.xml | 5 + .../src/main/res/values-ro/translations.xml | 5 + .../src/main/res/values-es/translations.xml | 14 ++ .../src/main/res/values-it/translations.xml | 14 ++ .../src/main/res/values-ro/translations.xml | 14 ++ .../src/main/res/values-es/translations.xml | 25 +++ .../src/main/res/values-it/translations.xml | 21 +++ .../src/main/res/values-ro/translations.xml | 22 +++ .../impl/src/main/res/values/localazy.xml | 6 + .../src/main/res/values-es/translations.xml | 61 +++++++ .../src/main/res/values-it/translations.xml | 61 +++++++ .../src/main/res/values-ro/translations.xml | 61 +++++++ .../src/main/res/values-es/translations.xml | 19 +++ .../src/main/res/values-it/translations.xml | 19 +++ .../src/main/res/values-ro/translations.xml | 19 +++ .../src/main/res/values-es/translations.xml | 17 ++ .../src/main/res/values-it/translations.xml | 17 ++ .../src/main/res/values-ro/translations.xml | 17 ++ .../src/main/res/values-de/translations.xml | 1 + .../src/main/res/values-es/translations.xml | 156 ++++++++++++++++++ .../src/main/res/values-fr/translations.xml | 1 + .../src/main/res/values-it/translations.xml | 148 +++++++++++++++++ .../src/main/res/values-ro/translations.xml | 150 ++++++++++++++++- .../src/main/res/values/localazy.xml | 11 +- 37 files changed, 995 insertions(+), 11 deletions(-) create mode 100644 features/createroom/impl/src/main/res/values-es/translations.xml create mode 100644 features/createroom/impl/src/main/res/values-it/translations.xml create mode 100644 features/createroom/impl/src/main/res/values-ro/translations.xml create mode 100644 features/login/impl/src/main/res/values-es/translations.xml create mode 100644 features/login/impl/src/main/res/values-it/translations.xml create mode 100644 features/login/impl/src/main/res/values-ro/translations.xml create mode 100644 features/logout/api/src/main/res/values-es/translations.xml create mode 100644 features/logout/api/src/main/res/values-it/translations.xml create mode 100644 features/logout/api/src/main/res/values-ro/translations.xml create mode 100644 features/onboarding/impl/src/main/res/values-es/translations.xml create mode 100644 features/onboarding/impl/src/main/res/values-it/translations.xml create mode 100644 features/onboarding/impl/src/main/res/values-ro/translations.xml create mode 100644 features/rageshake/api/src/main/res/values-es/translations.xml create mode 100644 features/rageshake/api/src/main/res/values-it/translations.xml create mode 100644 features/rageshake/api/src/main/res/values-ro/translations.xml create mode 100644 features/rageshake/impl/src/main/res/values-es/translations.xml create mode 100644 features/rageshake/impl/src/main/res/values-it/translations.xml create mode 100644 features/rageshake/impl/src/main/res/values-ro/translations.xml create mode 100644 features/roomdetails/impl/src/main/res/values-es/translations.xml create mode 100644 features/roomdetails/impl/src/main/res/values-it/translations.xml create mode 100644 features/roomdetails/impl/src/main/res/values-ro/translations.xml create mode 100644 features/roomlist/impl/src/main/res/values-es/translations.xml create mode 100644 features/roomlist/impl/src/main/res/values-it/translations.xml create mode 100644 features/roomlist/impl/src/main/res/values-ro/translations.xml create mode 100644 features/verifysession/impl/src/main/res/values-es/translations.xml create mode 100644 features/verifysession/impl/src/main/res/values-it/translations.xml create mode 100644 features/verifysession/impl/src/main/res/values-ro/translations.xml create mode 100644 libraries/textcomposer/src/main/res/values-es/translations.xml create mode 100644 libraries/textcomposer/src/main/res/values-it/translations.xml create mode 100644 libraries/textcomposer/src/main/res/values-ro/translations.xml create mode 100644 libraries/ui-strings/src/main/res/values-es/translations.xml create mode 100644 libraries/ui-strings/src/main/res/values-it/translations.xml diff --git a/features/createroom/impl/src/main/res/values-es/translations.xml b/features/createroom/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..f6248df74e3 --- /dev/null +++ b/features/createroom/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,6 @@ + + + "Nueva sala" + "Invitar gente" + "Añadir personas" + \ No newline at end of file diff --git a/features/createroom/impl/src/main/res/values-it/translations.xml b/features/createroom/impl/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..ea0c0b10e1f --- /dev/null +++ b/features/createroom/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,6 @@ + + + "Nuova stanza" + "Invita persone" + "Aggiungi persone" + \ No newline at end of file diff --git a/features/createroom/impl/src/main/res/values-ro/translations.xml b/features/createroom/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 00000000000..98839a883e0 --- /dev/null +++ b/features/createroom/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,6 @@ + + + "Cameră nouă" + "Invitați persoane" + "Adaugați persoane" + \ No newline at end of file diff --git a/features/login/impl/src/main/res/values-es/translations.xml b/features/login/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..a299083994d --- /dev/null +++ b/features/login/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,20 @@ + + + "No hemos podido acceder a este servidor. Comprueba que has introducido correctamente la dirección del servidor. Si la dirección es correcta, ponte en contacto con el administrador del servidor para obtener más ayuda." + "Este servidor no soporta sliding sync." + "Dirección del homeserver" + "Solo puedes conectarte a un servidor que soporte sliding sync. El administrador de tu servidor tendrá que configurarlo. %1$s" + "Continuar" + "¿Cuál es la dirección de tu servidor?" + "Selecciona tu servidor" + "Esta cuenta ha sido desactivada." + "Usuario y/o contraseña incorrectos" + "Este no es un id de usuario válido. Formato esperado: \'@user:homeserver.org\'" + "El servidor seleccionado no admite contraseñas ni inicio de sesión OIDC. Póngase en contacto con su administrador o elija otro homeserver." + "Introduce tus datos" + "Contraseña" + "Donde viven tus conversaciones" + "Continuar" + "¡Hola de nuevo!" + "Usuario" + \ No newline at end of file diff --git a/features/login/impl/src/main/res/values-it/translations.xml b/features/login/impl/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..429f1568836 --- /dev/null +++ b/features/login/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,20 @@ + + + "Non siamo riusciti a raggiungere questo homserver. Verifica di aver inserito correttamente l\'URL del server domestico. Se l\'URL è corretto, contatta l\'amministratore del tuo server domestico per ulteriore assistenza." + "Questo server attualmente non supporta la sincronizzazione scorrevole." + "URL dell\'homeserver" + "Puoi connetterti solo a un server esistente che supporta la sincronizzazione scorrevole. L\'amministratore del tuo server domestico dovrà configurarlo. %1$s" + "Continua" + "Qual è l\'indirizzo del tuo server?" + "Seleziona il tuo server" + "Questo profilo è stato disattivato." + "Nome utente e/o password errati" + "Questo non è un identificatore utente valido. Formato previsto: \'@user:homeserver.org\'" + "L\'homeserver selezionato non supporta la password o l\'accesso OIDC. Contatta il tuo amministratore o scegli un altro homeserver." + "Inserisci i tuoi dati" + "Password" + "Dove vivono le tue conversazioni" + "Continua" + "Bentornato!" + "Nome utente" + \ No newline at end of file diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 00000000000..2b5cce6829a --- /dev/null +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,20 @@ + + + "Nu am putut accesa acest homeserver. Te rugăm să verifici că ai introdus corect adresa URL a homeserver-ului. Dacă adresa URL este corectă, contactează administratorul homeserver-ului pentru ajutor suplimentar." + "Momentan acest server nu oferă suport pentru sliding sync." + "Adresa URL a homeserver-ului" + "Vă putețo conecta numai la un server existent care oferă suport pentru sliding sync. Administratorul homeserver-ului dumneavoastră va trebui să îl configureze. %1$s" + "Continuați" + "Care este adresa serverului dumneavoastră?" + "Selectați serverul" + "Acest cont a fost dezactivat." + "Utilizator și/sau parolă incorecte" + "Acesta nu este un identificator de utilizator valid. Format așteptat: „@user:homeserver.org”" + "Homeserver-ul selectat nu acceptă autentificarea prin parola sau OIDC. Te rugăm să contactezi administratorul sau să alegi un alt homeserver." + "Introduceți detaliile" + "Parolă" + "Locul unde trăiesc conversațiile tale" + "Continuați" + "Bine ați revenit!" + "Utilizator" + \ No newline at end of file diff --git a/features/logout/api/src/main/res/values-es/translations.xml b/features/logout/api/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..9072ab88a20 --- /dev/null +++ b/features/logout/api/src/main/res/values-es/translations.xml @@ -0,0 +1,8 @@ + + + "¿Estás seguro de que quieres cerrar sesión?" + "Cerrar sesión" + "Cerrar sesión" + "Cerrando sesión…" + "Cerrar sesión" + \ No newline at end of file diff --git a/features/logout/api/src/main/res/values-it/translations.xml b/features/logout/api/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..8b01a027800 --- /dev/null +++ b/features/logout/api/src/main/res/values-it/translations.xml @@ -0,0 +1,8 @@ + + + "Sei sicuro di voler uscire?" + "Esci" + "Esci" + "Uscita in corso..." + "Esci" + \ No newline at end of file diff --git a/features/logout/api/src/main/res/values-ro/translations.xml b/features/logout/api/src/main/res/values-ro/translations.xml new file mode 100644 index 00000000000..8befb1b1dd5 --- /dev/null +++ b/features/logout/api/src/main/res/values-ro/translations.xml @@ -0,0 +1,8 @@ + + + "Sunteți sigur că vreți să vă deconectați?" + "Deconectați-vă" + "Deconectați-vă" + "Deconectare în curs…" + "Deconectați-vă" + \ No newline at end of file diff --git a/features/onboarding/impl/src/main/res/values-es/translations.xml b/features/onboarding/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..235fb4558ae --- /dev/null +++ b/features/onboarding/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,5 @@ + + + "Bienvenido a la beta de %1$s. Vitaminado, para mayor rapidez y sencillez." + "Siéntente en tu Elemento" + \ No newline at end of file diff --git a/features/onboarding/impl/src/main/res/values-it/translations.xml b/features/onboarding/impl/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..cd3c6a696cd --- /dev/null +++ b/features/onboarding/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,5 @@ + + + "Benvenuto nella beta di %1$s. Potenziato in velocità e semplicità." + "Sii nel tuo elemento" + \ No newline at end of file diff --git a/features/onboarding/impl/src/main/res/values-ro/translations.xml b/features/onboarding/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 00000000000..03d967ab756 --- /dev/null +++ b/features/onboarding/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,5 @@ + + + "Bun venit la versiunea beta a %1$s. Supraalimentat, pentru viteză și simplitate." + "Fii în Elementul tău" + \ No newline at end of file diff --git a/features/rageshake/api/src/main/res/values-es/translations.xml b/features/rageshake/api/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..26ff483b91c --- /dev/null +++ b/features/rageshake/api/src/main/res/values-es/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s se cerró inesperadamente la última vez que se lo usaste. ¿Quieres compartir un informe de error con nosotros?" + "Parece que sacudes el teléfono con frustración. ¿Quieres abrir la pantalla de informe de errores?" + \ No newline at end of file diff --git a/features/rageshake/api/src/main/res/values-it/translations.xml b/features/rageshake/api/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..e6ef37d287b --- /dev/null +++ b/features/rageshake/api/src/main/res/values-it/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s si è chiuso inaspettatamente l\'ultima volta che è stato usato. Vuoi condividere con noi un rapporto sull\'arresto anomalo?" + "Sembra che tu stia scuotendo il telefono per la frustrazione. Vuoi aprire la schermata di segnalazione dei problemi?" + \ No newline at end of file diff --git a/features/rageshake/api/src/main/res/values-ro/translations.xml b/features/rageshake/api/src/main/res/values-ro/translations.xml new file mode 100644 index 00000000000..17180d5145a --- /dev/null +++ b/features/rageshake/api/src/main/res/values-ro/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s s-a blocat ultima dată când a fost folosit. Doriți să ne trimiteți un raport?" + "Se pare că scuturați telefonul de frustrare. Doriți să deschdeți ecranul de raportare a unei erori?" + \ No newline at end of file diff --git a/features/rageshake/impl/src/main/res/values-es/translations.xml b/features/rageshake/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..527376f2680 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,14 @@ + + + "Adjuntar captura de pantalla" + "Podéis poneros en contacto conmigo para resolver dudas relacionadas" + "Editar captura de pantalla" + "Describe el problema. ¿Qué hiciste? ¿Qué esperabas que ocurriera? ¿Qué ocurrió en realidad? Por favor, detállalo todo lo que puedas." + "Describe el error..." + "Si es posible, escriba la descripción en inglés." + "Enviar registros de fallos" + "Enviar registros para ayudar" + "Enviar captura de pantalla" + "Para comprobar que todo funciona correctamente, se enviarán registros de fallos con su mensaje. Serán privados. Para enviar sólo tu mensaje, desactiva esta opción." + "%1$s se cerró inesperadamente la última vez que se lo usaste. ¿Quieres compartir un informe de error con nosotros?" + \ No newline at end of file diff --git a/features/rageshake/impl/src/main/res/values-it/translations.xml b/features/rageshake/impl/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..60397f87199 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,14 @@ + + + "Allega istantanea schermo" + "Potete contattarmi per qualsiasi altra domanda" + "Modifica istantanea schermo" + "Descrivi il bug. Che cosa hai fatto? Cosa ti aspettavi che accadesse? Cosa è effettivamente accaduto. Si prega di inserire il maggior numero di dettagli possibile." + "Descrivi il problema..." + "Se possibile, scrivere la descrizione in inglese." + "Invia i log degli arresti anomali" + "Invia i log per aiutarci" + "Invia istantanea schermo" + "Per verificare che le cose funzionino come previsto, i log verranno inviati con il tuo messaggio. Questi saranno privati. Per inviare solo il tuo messaggio, disattiva questa impostazione." + "%1$s si è chiuso inaspettatamente l\'ultima volta che è stato usato. Vuoi condividere con noi un rapporto sull\'arresto anomalo?" + \ No newline at end of file diff --git a/features/rageshake/impl/src/main/res/values-ro/translations.xml b/features/rageshake/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 00000000000..6b66ea417e2 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,14 @@ + + + "Atașați o captură de ecran" + "Puteți să mă contactați dacă aveți întrebări suplimentare" + "Editați captura de ecran" + "Vă rugăm să descrieți eroarea. Ce ați făcut? Ce vă aşteptați să se întâmple? Ce s-a întâmplat de fapt. Vă rugam să intrați în cât mai multe detalii cu putință." + "Descrieți eroarea…" + "Dacă posibil, vă rugăm să scrieți descrierea în engleză." + "Trimiteți log-uri" + "Trimiteți log-uri pentru a ajuta" + "Trimiteți captură de ecran" + "Pentru a verifica că lucrurile funcționează conform așteptărilor, log-uri vor fi trimise împreună cu mesajul. Acestea vor fi private. Pentru a trimite doar mesajul, dezactivați această setare." + "%1$s s-a blocat ultima dată când a fost folosit. Dorești să ne trimiti un raport?" + \ No newline at end of file diff --git a/features/roomdetails/impl/src/main/res/values-es/translations.xml b/features/roomdetails/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..817d3613ae8 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,25 @@ + + + + "0 personas" + "Una persona" + + + + "%1$d personas" + + "Bloquear" + "Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puedes revertir esta acción en cualquier momento." + "Bloquear usuario" + "Desbloquear" + "Al desbloquear al usuario, podrás volver a ver todos sus mensajes." + "Desbloquear usuario" + "Los mensajes están protegidos con \"candados\". Sólo tú y los destinatarios tenéis las llaves únicas para abrirlos." + "Cifrado de mensajes activado" + "Invitar a otras personas" + "Salir de la sala" + "Personas" + "Seguridad" + "Compartir sala" + "Tema" + \ No newline at end of file diff --git a/features/roomdetails/impl/src/main/res/values-it/translations.xml b/features/roomdetails/impl/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..9a980b79a9a --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,21 @@ + + + + "1 persona" + "%1$d persone" + + "Blocca" + "Gli utenti bloccati non saranno in grado di inviarti messaggi e tutti i loro messaggi saranno nascosti. Potrai annullare questa azione in qualsiasi momento." + "Blocca utente" + "Sblocca" + "Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi." + "Sblocca utente" + "I messaggi sono protetti da lucchetti. Solo tu e i destinatari avete le chiavi univoche per sbloccarli." + "Crittografia messaggi abilitata" + "Invita persone" + "Esci dalla stanza" + "Persone" + "Sicurezza" + "Condividi stanza" + "Oggetto" + \ No newline at end of file diff --git a/features/roomdetails/impl/src/main/res/values-ro/translations.xml b/features/roomdetails/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 00000000000..db6777fb7f2 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,22 @@ + + + + "o persoană" + + "%1$d persoane" + + "Blocați" + "Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând." + "Blocați utilizatorul" + "Deblocați" + "La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta." + "Deblocați utilizatorul" + "Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca." + "Criptarea mesajelor este activată" + "Invitați persoane" + "Părăsiți camera" + "Persoane" + "Securitate" + "Partajați camera" + "Subiect" + \ No newline at end of file diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index 82c0c1c418f..f63757a8e33 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -4,6 +4,12 @@ "1 person" "%1$d people" + "Block" + "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime." + "Block user" + "Unblock" + "On unblocking the user, you will be able to see all messages by them again." + "Unblock user" "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them." "Message encryption enabled" "Invite people" diff --git a/features/roomlist/impl/src/main/res/values-es/translations.xml b/features/roomlist/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..079ffb2e723 --- /dev/null +++ b/features/roomlist/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,61 @@ + + + "Crear una nueva conversación o sala" + "Todos los chats" + "Parece que estás usando un nuevo dispositivo. Verifica que eres tú para acceder a tus mensajes cifrados." + "Accede a tu historial de mensajes" + "(el avatar también cambió)" + "%1$s cambió su avatar" + "Cambiaste tu avatar" + "%1$s cambió su nombre de %2$s a %3$s" + "Cambiaste tu nombre de %1$s a %2$s" + "%1$s eliminó su nombre (era %2$s)" + "Eliminaste tu nombre (era %1$s)" + "%1$s cambió su nombre a %2$s" + "Cambiaste tu nombre a %1$s" + "%1$s cambió el avatar de la sala" + "Cambiaste el avatar de la sala" + "%1$s eliminó el avatar de la sala" + "Eliminaste el avatar de la sala" + "%1$s expulsó permanentemente a %2$s" + "Expulsaste permanentemente a %1$s" + "%1$s creó la sala" + "Tú creaste la sala" + "%1$s invitó a %2$s" + "%1$s aceptó la invitación" + "Aceptaste la invitación" + "Invitaste a %1$s" + "%1$s te invitó." + "%1$s se unió a la sala" + "Te uniste a la sala" + "%1$s solicitó unirse" + "%1$s permitió que %2$s se uniera" + "%1$s te permitió unirte" + "Solicitaste unirte" + "%1$s rechazó la solicitud de %2$s para unirse" + "Rechazaste la solicitud de %1$s para unirte" + "%1$s rechazó su solicitud para unirte" + "%1$s ya no está interesado en unirse" + "Cancelaste tu solicitud de unirte" + "%1$s salió de la sala" + "Saliste de la sala" + "%1$s cambió el nombre de la sala a: %2$s" + "Cambiaste el nombre de la sala a: %1$s" + "%1$s eliminó el nombre de la sala" + "Eliminaste el nombre de la sala" + "%1$s rechazó la invitación" + "Rechazaste la invitación" + "%1$s echó a %2$s" + "Echaste a %1$s" + "%1$s envió una invitación a %2$s para unirse a la sala" + "Enviaste una invitación a %1$s para unirse a la sala" + "%1$s revocó la invitación a %2$s para unirse a la sala" + "Revocaste la invitación de %1$s para unirse a la sala" + "%1$s cambió el tema a: %2$s" + "Cambiaste el tema a: %1$s" + "%1$s eliminó el tema de la sala" + "Eliminaste el tema de la sala" + "%1$s readmitió a %2$s" + "Readmitiste a %1$s" + "%1$s realizó un cambio desconocido en su membresía" + \ No newline at end of file diff --git a/features/roomlist/impl/src/main/res/values-it/translations.xml b/features/roomlist/impl/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..20bf4879379 --- /dev/null +++ b/features/roomlist/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,61 @@ + + + "Crea una nuova conversazione o stanza" + "Tutte le conversazioni" + "Sembra che tu stia utilizzando un nuovo dispositivo. Verifica di essere tu per accedere ai tuoi messaggi crittografati." + "Accedi alla cronologia dei messaggi" + "(anche l\'avatar è stato cambiato)" + "%1$s ha cambiato il proprio avatar" + "Hai cambiato il tuo avatar" + "%1$s ha cambiato il proprio nome visualizzato da %2$s a %3$s" + "Hai cambiato il tuo nome visualizzato da %1$s a %2$s" + "%1$s ha rimosso il proprio nome visualizzato (era %2$s)" + "Hai rimosso il tuo nome visualizzato (era %1$s)" + "%1$s ha impostato il proprio nome visualizzato su %2$s" + "Hai impostato il tuo nome visualizzato su %1$s" + "%1$s ha cambiato l\'avatar della stanza" + "Hai cambiato l\'avatar della stanza" + "%1$s ha rimosso l\'avatar della stanza" + "Hai rimosso l\'avatar della stanza" + "%1$s ha rimosso %2$s" + "Hai rimosso %1$s" + "%1$s ha creato la stanza" + "Hai creato la stanza" + "%1$s ha invitato %2$s" + "%1$s ha accettato l\'invito" + "Hai accettato l\'invito" + "Hai invitato %1$s" + "%1$s ti ha invitato" + "%1$s si è unito alla stanza" + "Ti sei unito alla stanza" + "%1$s ha chiesto di unirsi" + "%1$s ha permesso a %2$s di unirsi" + "%1$s ti ha permesso di unirti" + "Hai richiesto di unirti" + "%1$s ha rifiutato la richiesta di unirsi di %2$s" + "Hai rifiutato la richiesta di unirsi di %1$s" + "%1$s ha rifiutato la tua richiesta di unirti" + "%1$s non è più interessato a partecipare" + "Hai annullato la tua richiesta di unirti" + "%1$s ha lasciato la stanza" + "Hai lasciato la stanza" + "%1$s ha cambiato il nome della stanza in: %2$s" + "Hai cambiato il nome della stanza in: %1$s" + "%1$s ha rimosso il nome della stanza" + "Hai rimosso il nome della stanza" + "%1$s ha rifiutato l\'invito" + "Hai rifiutato l\'invito" + "%1$s ha rimosso %2$s" + "Hai rimosso %1$s" + "%1$s ha inviato un invito a %2$s per unirsi alla stanza" + "Hai inviato un invito a %1$s per unirsi alla stanza" + "%1$s ha revocato l\'invito di %2$s ad unirsi alla stanza." + "Hai revocato l\'invito a %1$s a universi alla stanza" + "%1$s ha cambiato l\'oggetto in: %2$s" + "Hai cambiato l\'oggetto in: %1$s" + "%1$s ha rimosso l\'oggetto della stanza" + "Hai rimosso l\'oggetto della stanza" + "%1$s ha sbloccato %2$s" + "Hai sbloccato %1$s" + "%1$s ha apportato una modifica sconosciuta alla propria iscrizione" + \ No newline at end of file diff --git a/features/roomlist/impl/src/main/res/values-ro/translations.xml b/features/roomlist/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 00000000000..89760b34973 --- /dev/null +++ b/features/roomlist/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,61 @@ + + + "Creați o conversație sau o cameră nouă" + "Toate conversatiile" + "Se pare că folosiți un dispozitiv nou. Verificați-vă identitatea pentru acces la mesajele dumneavoastră criptate." + "Accesați istoricul mesajelor" + "(s-a schimbat si avatarul)" + "%1$s și-a schimbat avatarul" + "V-ați schimbat avatarul" + "%1$s și-a schimbat numele din %2$s în %3$s" + "V-ați schimbat numele din %1$s în %2$s" + "%1$s și-a sters numele (era %2$s)" + "V-ați sters numele (era %1$s)" + "%1$s și-a schimbat numele %2$s" + "V-ați schimbat numele în %1$s" + "%1$s a schimbat avatarul camerei" + "Ați schimbat avatarul camerei" + "%1$s a șters avatarul camerei" + "Ați șters avatarul camerei" + "%1$s a adăugat o interdicție pentru %2$s" + "Ați adăugat o interdicție pentru %1$s" + "%1$s a creat camera" + "Ați creat camera" + "%1$s l-a invitat pe %2$s" + "%1$s a acceptat invitația" + "Ați acceptat invitația" + "L-ați invitat pe %1$s" + "%1$s v-a invitat" + "%1$s a intrat în cameră" + "Ați intrat în cameră" + "%1$s a solicitat să se alăture camerei" + "%1$s i-a permis lui %2$s să se alăture camerei" + "%1$s v-a permis să vă alăturați camerei" + "Ați solicitat să vă alăturați camerei" + "%1$s a respins solicitarea de alăturare a lui %2$s" + "Ați respins solicitarea de alăturare a lui %1$s" + "%1$s a respins cererea dumneavoastră de alăturare" + "%1$s nu mai este interesat să se alăture camerei" + "Ați anulat cererea de alăturare" + "%1$s a părăsit camera" + "Ați părăsit camera" + "%1$s a schimbat numele camerei în: %2$s" + "Ați schimbat numele camerei în: %1$s" + "%1$s a sters numele camerei" + "Ați șters numele camerei" + "%1$s a respins invitația" + "Ați respins invitația" + "%1$s l-a îndepărtat pe %2$s" + "L-ați îndepărtat pe %1$s" + "%1$s a trimis o invitație către %2$s pentru a se alătura camerei" + "Ați trimis o invitație către %1$s pentru a se alătura camerei" + "%1$s a revocat invitația pentru %2$s de a se alătura camerei" + "Ați revocat invitația pentru %1$s de a se alătura camerei" + "%1$s a schimbat subiectul în: %2$s" + "Ați schimbat subiectul în: %1$s" + "%1$s a șters subiectul camerei" + "Ați șters subiectul camerei" + "%1$s a anulat interdicția pentru %2$s" + "Ați anulat interdicția pentru %1$s" + "%1$s a făcut o modificare necunoscută asupra calității sale de membru" + \ No newline at end of file diff --git a/features/verifysession/impl/src/main/res/values-es/translations.xml b/features/verifysession/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..839c945e24a --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,19 @@ + + + "Algo no fue bien. Se agotó el tiempo de espera de la solicitud o se rechazó." + "Verificación cancelada" + "Confirma que los emojis que aparecen a continuación coinciden con los que aparecen en tu otra sesión." + "Comparar emojis" + "Tu nueva sesión ya está verificada. Tienes acceso a tus mensajes cifrados y otros usuarios lo considerarán de confianza." + "Demuestra que eres tú para acceder a tu historial de mensajes cifrados." + "Abrir una sesión existente" + "Reintentar la verificación" + "Estoy listo" + "Comenzar" + "Esperando a que coincida" + "Compara los emoji, asegurándote de que aparecen en el mismo orden." + "No coinciden" + "Coinciden" + "Acepta la solicitud para iniciar el proceso de verificación en tu otra sesión para continuar." + "A la espera de aceptar la solicitud" + \ No newline at end of file diff --git a/features/verifysession/impl/src/main/res/values-it/translations.xml b/features/verifysession/impl/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..3d8a46d5818 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,19 @@ + + + "C\'è qualcosa che non va. La richiesta è scaduta o è stata rifiutata." + "Verifica annullata" + "Verifica che gli emoji sottostanti corrispondano a quelli mostrati nell\'altra sessione." + "Confronta le emoji" + "La tua nuova sessione è ora verificata. Ha accesso ai tuoi messaggi crittografati e gli altri utenti la vedranno come attendibile." + "Dimostra la tua identità per accedere alla cronologia dei messaggi crittografati." + "Apri una sessione esistente" + "Riprova la verifica" + "Sono pronto" + "Inizia" + "In attesa di un riscontro" + "Confronta le emoji uniche, assicurandoti che appaiano nello stesso ordine." + "Non corrispondono" + "Corrispondono" + "Accetta la richiesta di avviare il processo di verifica nell\'altra sessione per continuare." + "In attesa di accettare la richiesta" + \ No newline at end of file diff --git a/features/verifysession/impl/src/main/res/values-ro/translations.xml b/features/verifysession/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 00000000000..f2bade56fc4 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,19 @@ + + + "Ceva nu este în regulă. Fie cererea a expirat, fie a fost respinsă." + "Verificare anulată" + "Confirmați că emoticoanele de mai jos se potrivesc cu cele afișate în cealaltă sesiune." + "Comparați emoticoanele" + "Noua dumneavoastră sesiune este acum verificată. Are acces la mesajele dumneavoastră criptate, iar alți utilizatori vă vor vedea ca fiind de încredere." + "Demonstrați-vă identitatea pentru a accesa istoricul mesajelor criptate." + "Deschideți o sesiune existentă" + "Reîncercați verificarea" + "Sunt pregătit" + "Începeți" + "Se așteaptă confirmarea" + "Comparăți emoticoalene asigurându-vă că apar în aceeași ordine." + "Nu se potrivesc" + "Se potrivesc" + "Acceptați solicitarea de a începe procesul de verificare în cealaltă sesiune pentru a continua." + "Se așteptă acceptarea cererii" + \ No newline at end of file diff --git a/libraries/textcomposer/src/main/res/values-es/translations.xml b/libraries/textcomposer/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..7392bd2b31a --- /dev/null +++ b/libraries/textcomposer/src/main/res/values-es/translations.xml @@ -0,0 +1,17 @@ + + + "Lista de puntos" + "Bloque de código" + "Mensaje..." + "Aplicar formato negrita" + "Aplicar formato cursiva" + "Aplicar formato tachado" + "Aplicar formato de subrayado" + "Pantalla completa" + "Añadir sangría" + "Código" + "Enlazar" + "Lista numérica" + "Cita" + "Quitar sangría" + \ No newline at end of file diff --git a/libraries/textcomposer/src/main/res/values-it/translations.xml b/libraries/textcomposer/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..54ca270f288 --- /dev/null +++ b/libraries/textcomposer/src/main/res/values-it/translations.xml @@ -0,0 +1,17 @@ + + + "Attiva/disattiva l\'elenco puntato" + "Attiva/disattiva il blocco di codice" + "Messaggio…" + "Applica il formato in grassetto" + "Applicare il formato corsivo" + "Applica il formato barrato" + "Applicare il formato di sottolineatura" + "Attiva/disattiva la modalità a schermo intero" + "Rientro a destra" + "Applicare il formato del codice in linea" + "Imposta collegamento" + "Attiva/disattiva elenco numerato" + "Attiva/disattiva citazione" + "Rientro a sinistra" + \ No newline at end of file diff --git a/libraries/textcomposer/src/main/res/values-ro/translations.xml b/libraries/textcomposer/src/main/res/values-ro/translations.xml new file mode 100644 index 00000000000..b053e0ecaa1 --- /dev/null +++ b/libraries/textcomposer/src/main/res/values-ro/translations.xml @@ -0,0 +1,17 @@ + + + "Comutați lista cu puncte" + "Comutați blocul de cod" + "Mesaj…" + "Aplicați formatul aldin" + "Aplicați formatul italic" + "Aplicați formatul barat" + "Aplică formatul de subliniere" + "Comutați modul ecran complet" + "Indentare" + "Aplicați formatul de cod inline" + "Setați linkul" + "Comutați lista numerotată" + "Aplicați citatul" + "Dez-identare" + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 64684b79052..22c60db4813 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -1,4 +1,5 @@ "Bestätigen" + "de" \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..d05dc300d2a --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -0,0 +1,156 @@ + + + "Ocultar contraseña" + "Enviar archivos" + "Mostrar contraseña" + "Menú de usuario" + "Atrás" + "Cancelar" + "Borrar" + "Cerrar" + "Completar verificación" + "Confirmar" + "Continuar" + "Copiar" + "Copiar enlace" + "Crear una sala" + "Desactivar" + "Hecho" + "Editar" + "Activar" + "Invitar" + "Invitar amigos a %1$s" + "Más información" + "Salir" + "Salir de la sala" + "Siguiente" + "No" + "Ahora no" + "OK" + "Respuesta rápida" + "Citar" + "Eliminar" + "Responder" + "Informar de un error" + "Reportar Contenido" + "Reintentar" + "Reintentar descifrado" + "Guardar" + "Buscar" + "Enviar" + "Compartir" + "Compartir enlace" + "Saltar" + "Comenzar" + "Iniciar chat" + "Iniciar la verificación" + "Ver Fuente" + "Sí" + "Acerca de" + "Sonido" + "Burbujas" + "Creando sala..." + "Saliste de la sala" + "Error de descifrado" + "Opciones de desarrollador" + "(editado)" + "Edición" + "Cifrado activado" + "Error" + "Archivo" + "GIF" + "Imagen" + "Enlace copiado al portapapeles" + "Cargando…" + "Mensaje" + "Diseño del mensaje" + "Mensaje eliminado" + "Moderno" + "No hay resultados" + "Sin conexión" + "Contraseña" + "Personas" + "Enlace permanente" + "Reacciones" + "Respondiendo a %1$s" + "Informar de un error" + "Informe enviado" + "Buscar a alguien" + "Seguridad" + "Selecciona tu servidor" + "Enviando…" + "Servidor no compatible" + "Dirección del servidor" + "Ajustes" + "Sticker" + "Terminado" + "Sugerencias" + "Tema" + "No se puede descifrar" + "Evento no compatible" + "Usuario" + "Verificación cancelada" + "Verificación completada" + "Vídeo" + "Esperando..." + "Confirmar" + "Error" + "Terminado" + "Atención" + "Actividades" + "Banderas" + "Comida y bebida" + "Animales y naturaleza" + "Objetos" + "Emojis y personas" + "Viajes y lugares" + "Símbolos" + "No se pudo crear el enlace permanente" + "Error al cargar mensajes" + "No se encontró ninguna aplicación compatible con esta acción." + "Algunos mensajes no se han enviado" + "Lo siento, se ha producido un error" + "Hola, puedes hablar conmigo en %1$s: %2$s" + "¿Estás seguro de que quieres salir de esta sala? Eres la única persona aquí. Si te vas, nadie podrá unirse en el futuro, ni siquiera tú." + "¿Estás seguro de que quieres abandonar esta sala? Esta sala no es pública y no podrás volver a entrar sin una invitación." + "¿Seguro que quieres salir de la habitación?" + "%1$s Android" + + "No hay miembros" + "%1$d miembro" + + + + "%1$d miembros" + + + "No hay cambios en la sala" + "%1$d cambio en la sala" + + + + "%1$d cambios en la sala" + + "Agitar con fuerza para informar de un error" + "Parece que sacudes el teléfono con frustración. ¿Quieres abrir la pantalla de informe de errores?" + "Este mensaje se notificará al administrador de su homeserver. No podrán leer ningún mensaje cifrado." + "Motivo para denunciar este contenido" + "Este es el principio de %1$s." + "Este es el principio de esta conversación." + "Nuevos" + "Bloquear usuario" + "Marque si quieres ocultar todos los mensajes actuales y futuros de este usuario" + "Bloquear" + "Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento." + "Bloquear usuario" + "Desbloquear" + "Al desbloquear al usuario, podrás volver a ver todos sus mensajes." + "Desbloquear usuario" + "Se ha producido un error al intentar iniciar un chat" + "No podemos validar el ID de Matrix de este usuario. Es posible que no reciba la invitación." + "Agitar con fuerza" + "Umbral de detección" + "General" + "Versión: %1$s (%2$s)" + "es" + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index b75da2740b6..5bdce47d20b 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -1,4 +1,5 @@ "Confirmer" + "fr" \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..80fed2e6f83 --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -0,0 +1,148 @@ + + + "Nascondi password" + "Invia file" + "Mostra password" + "Menu utente" + "Indietro" + "Annulla" + "Cancella" + "Chiudi" + "Completa verifica" + "Conferma" + "Continua" + "Copia" + "Copia collegamento" + "Crea una stanza" + "Disabilita" + "Fine" + "Modifica" + "Attiva" + "Invita" + "Invita amici a %1$s" + "Ulteriori informazioni" + "Esci" + "Esci dalla stanza" + "Avanti" + "No" + "Non ora" + "OK" + "Risposta rapida" + "Citazione" + "Rimuovi" + "Rispondi" + "Segnala un problema" + "Segnala Contenuto" + "Riprova" + "Riprova la decrittazione" + "Salva" + "Ricerca" + "Invia" + "Condividi" + "Condividi collegamento" + "Salta" + "Inizia" + "Avvia conversazione" + "Avvia la verifica" + "Vedi Sorgente" + "Sì" + "Informazioni" + "Audio" + "Fumetti" + "Creazione stanza..." + "Hai lasciato la stanza" + "Errore di decrittazione" + "Opzioni sviluppatore" + "(modificato)" + "Modifica in corso" + "Crittografia abilitata" + "Errore" + "File" + "GIF" + "Immagine" + "Collegamento copiato negli appunti" + "Caricamento…" + "Messaggio" + "Layout del messaggio" + "Messaggio rimosso" + "Moderno" + "Nessun risultato" + "Non in linea" + "Password" + "Persone" + "Collegamento permanente" + "Reazioni" + "Risposta a %1$s" + "Segnala un problema" + "Segnalazione inviata" + "Cerca qualcuno" + "Sicurezza" + "Seleziona il tuo server" + "Invio in corso…" + "Server non supportato" + "URL del server" + "Impostazioni" + "Adesivo" + "Operazione riuscita" + "Suggerimenti" + "Oggetto" + "Impossibile decrittografare" + "Evento non supportato" + "Nome utente" + "Verifica annullata" + "Verifica completata" + "Video" + "In attesa…" + "Conferma" + "Errore" + "Operazione riuscita" + "Attenzione" + "Attività" + "Bandiere" + "Cibi & Bevande" + "Animali & Natura" + "Oggetti" + "Faccine & Persone" + "Viaggi & Luoghi" + "Simboli" + "Impossibile creare il collegamento permanente" + "Caricamento dei messaggi non riuscito" + "Non è stata trovata alcuna app compatibile per gestire questa azione." + "Alcuni messaggi non sono stati inviati" + "Siamo spiacenti, si è verificato un errore" + "Ehi, parlami su %1$s: %2$s" + "Sei sicuro di voler lasciare questa stanza? Sei l\'unica persona presente. Se esci, nessuno potrà unirsi in futuro, te compreso." + "Sei sicuro di voler lasciare questa stanza? Questa stanza non è pubblica e non potrai rientrare senza un invito." + "Sei sicuro di voler lasciare la stanza?" + "%1$s Android" + + "%1$d membro" + "%1$d membri" + + + "%1$d modifica alla stanza" + "%1$d modifiche alla stanza" + + "Scuoti per segnalare un problema" + "Sembra che tu stia scuotendo il telefono per la frustrazione. Vuoi aprire la schermata di segnalazione dei problemi?" + "Questo messaggio verrà segnalato all\'amministratore dell\'homeserver. Questi non sarà in grado di leggere i messaggi criptati." + "Motivo della segnalazione di questo contenuto" + "Questo è l\'inizio di %1$s." + "Questo è l\'inizio della conversazione." + "Nuovo" + "Blocca utente" + "Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente" + "Blocca" + "Gli utenti bloccati non saranno in grado di inviarti nuovi messaggi e tutti quelli già esistenti saranno nascosti. Potrai annullare questa azione in qualsiasi momento." + "Blocca utente" + "Sblocca" + "Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi." + "Sblocca utente" + "Si è verificato un errore durante il tentativo di avviare una chat" + "Non possiamo convalidare l\'ID Matrix di questo utente. L\'invito potrebbe non essere ricevuto." + "Rageshake" + "Soglia di rilevamento" + "Generali" + "Versione: %1$s (%2$s)" + "it" + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index 6878d7aab4e..1872bb057f7 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -1,10 +1,150 @@ - "Confirmare" + "Ascundeți parola" + "Trimiteți fișiere" + "Afișați parola" + "Meniu utilizator" + "Înapoi" + "Anulați" + "Ștergeți" + "Închideți" + "Verificare completă" + "Confirmați" + "Continuați" + "Copiați" + "Copiați linkul" "Creați o cameră" - "Gata" + "Dezactivați" + "Efectuat" + "Editați" + "Activați" + "Invitați" + "Invitați prieteni în %1$s" + "Aflați mai multe" + "Părăsiți" + "Părăsiți camera" + "Următorul" + "Nu" + "Nu acum" "OK" - "Raportează conținutul" - "Începe discuția" - "Vezi sursa" + "Raspuns rapid" + "Citat" + "Ștergeți" + "Răspundeți" + "Raportați o eroare" + "Raportați conținutul" + "Reîncercați" + "Reîncercați decriptarea" + "Salvați" + "Căutați" + "Trimiteți" + "Partajați" + "Partajați linkul" + "Omiteți" + "Începeți" + "Începeți discuția" + "Începeți verificarea" + "Vedeți sursă" + "Da" + "Despre" + "Audio" + "Baloane" + "Se creează camera…" + "Ați parăsit camera" + "Eroare de decriptare" + "Opțiuni programator" + "(editat)" + "Editare" + "Criptare activată" + "Eroare" + "Fişier" + "GIF" + "Imagine" + "Linkul a fost copiat în clipboard" + "Se încarcă…" + "Mesaj" + "Aranjamentul mesajelor" + "Mesaj sters" + "Modern" + "Niciun rezultat" + "Deconectat" + "Parola" + "Persoane" + "Permalink" + "Reacții" + "Răspuns pentru %1$s" + "Raportați o eroare" + "Raport trimis" + "Căutați pe cineva" + "Securitate" + "Selectați serverul" + "Se trimite…" + "Serverul nu este compatibil" + "Adresa URL a serverului" + "Setări" + "Autocolant" + "Succes" + "Sugestii" + "Subiect" + "Nu s-a putut decripta" + "Eveniment neacceptat" + "Utilizator" + "Verificare anulată" + "Verificare completă" + "Video" + "Se aşteaptă…" + "Confirmare" + "Eroare" + "Succes" + "Avertisment" + "Activități" + "Steaguri" + "Mâncare & Băutură" + "Animale și Natură" + "Obiecte" + "Fețe zâmbitoare & Oameni" + "Călătorii & Locuri" + "Simboluri" + "Crearea permalink-ului a eșuat" + "Încărcarea mesajelor a eșuat" + "Nu a fost găsită nicio aplicație capabilă să gestioneze această acțiune." + "Unele mesaje nu au fost trimise" + "Ne pare rău, a apărut o eroare" + "Hei, vorbește cu mine pe %1$s: %2$s" + "Sunteți sigur că vreți să părăsiți această cameră? Sunteți singura persoană de aici. Dacă o părasiți, nimeni nu se va mai putea alătura în viitor, inclusiv dumneavoastra." + "Sunteți sigur că vrei să părăsiți această cameră? Această cameră nu este publică și nu va veti putea alătura din nou fără o invitație." + "Sunteți sigur că vreți să părăsiți camera?" + "%1$s Android" + + "%1$d membru" + + "%1$d membri" + + + "%1$d schimbare a camerii" + + "%1$d schimbări ale camerei" + + "Rageshake pentru a raporta erori" + "Se pare că scuturați telefonul de frustrare. Doriți să deschdeți ecranul de raportare a unei erori?" + "Acest mesaj va fi raportat administratorilor homeserver-ului tau. Ei nu vor putea citi niciun mesaj criptat." + "Motivul raportării acestui conținut" + "Acesta este începutul conversației %1$s." + "Acesta este începutul acestei conversații." + "Nou" + "Blocați utilizatorul" + "Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator" + "Blocați" + "Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând." + "Blocați utilizatorul" + "Deblocați" + "La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta." + "Deblocați utilizatorul" + "A apărut o eroare la încercarea începerii conversației" + "Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită." + "Rageshake" + "Prag de detecție" + "General" + "Versiunea: %1$s (%2$s)" + "ro" \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 7ae90b9b53e..28cef98c609 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -38,6 +38,7 @@ "Save" "Search" "Send" + "Share" "Share link" "Skip" "Start" @@ -49,6 +50,7 @@ "Audio" "Bubbles" "Creating room…" + "Left room" "Decryption error" "Developer options" "(edited)" @@ -128,12 +130,6 @@ "This is the beginning of %1$s." "This is the beginning of this conversation." "New" - "Block" - "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime." - "Block user" - "Unblock" - "On unblocking the user, you will be able to see all messages by them again." - "Unblock user" "Block user" "Check if you want to hide all current and future messages from this user" "Block" @@ -143,8 +139,11 @@ "On unblocking the user, you will be able to see all messages by them again." "Unblock user" "An error occurred when trying to start a chat" + "We can’t validate this user’s Matrix ID. The invite might not be received." "Rageshake" "Detection threshold" "General" "Version: %1$s (%2$s)" + "en" + "en" \ No newline at end of file From 938e11f9682fb27163473dece07a490236af2156 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 11:22:53 +0200 Subject: [PATCH 025/104] Add key naming rules for dialogs. --- tools/localazy/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/localazy/README.md b/tools/localazy/README.md index ed47efd2c32..58bd1379326 100644 --- a/tools/localazy/README.md +++ b/tools/localazy/README.md @@ -27,6 +27,7 @@ For code clarity and in order to download strings to the correct module, here ar - Keys for common accessibility strings must start by `a11y_`. Example: `a11y_hide_password`; - Keys for strings used in a single screen must start with `screen_` followed by the screen name, followed by a free name. Example: `screen_onboarding_welcome_title`; - Keys can have `_title` or `_subtitle` suffixes. Example: `screen_onboarding_welcome_title`, `screen_change_server_subtitle`; +- For dialogs, keys can have `_dialog_title`, `_dialog_content`, and `_dialog_submit` suffixes. Example: `screen_signout_confirmation_dialog_title`, `screen_signout_confirmation_dialog_content`, `screen_signout_confirmation_dialog_submit`; - `a11y_` pattern can be used for strings that are only used for accessibility. Example: `a11y_hide_password`, `screen_roomlist_a11y_create_message`; - Strings for error message can start by `error_`, or contain `_error_` if used in a specific screen only. Example: `error_some_messages_have_not_been_sent`, `screen_change_server_error_invalid_homeserver`. From ae34033d731bedf3afca50fa2115e304c1a2fd00 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 11:33:46 +0200 Subject: [PATCH 026/104] Add a section about placeholders. --- tools/localazy/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/localazy/README.md b/tools/localazy/README.md index 58bd1379326..395961f91b0 100644 --- a/tools/localazy/README.md +++ b/tools/localazy/README.md @@ -7,6 +7,7 @@ Localazy is used to host the source strings and their translations. * [Localazy project](#localazy-project) * [Key naming rules](#key-naming-rules) * [Special suffixes](#special-suffixes) + * [Placeholders](#placeholders) * [CLI Installation](#cli-installation) * [Download translations](#download-translations) * [Add translations to a specific module](#add-translations-to-a-specific-module) @@ -40,6 +41,10 @@ For code clarity and in order to download strings to the correct module, here ar So feel free to use those suffixes when necessary for instance when the string content is referring to something related to Android only, or iOS only. +#### Placeholders + +Placeholders should have the form `%1$s`, `%1$d`, etc.. Please use numbered placeholders. Note that Localazy will take care of converting the placeholder to Android (-> `%1$s`) and iOS specific format (-> `%1$@`). Ideally add a comment on Localazy to explain with what the placeholder(s) will be replaced at runtime. + ## CLI Installation To install the Localazy client, follow the instructions from [here](https://localazy.com/docs/cli/installation). From ec6c5dc58b18bd577906dfc44b62445e0a85efca Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 11:44:55 +0200 Subject: [PATCH 027/104] Ensure ellipsis char is used (fix lint issue). --- .../logout/api/src/main/res/values-it/translations.xml | 2 +- .../impl/src/main/res/values-es/translations.xml | 2 +- .../impl/src/main/res/values-it/translations.xml | 2 +- .../textcomposer/src/main/res/values-es/translations.xml | 2 +- .../ui-strings/src/main/res/values-es/translations.xml | 4 ++-- .../ui-strings/src/main/res/values-it/translations.xml | 2 +- tools/localazy/generateLocalazyConfig.py | 9 +++++++++ 7 files changed, 16 insertions(+), 7 deletions(-) diff --git a/features/logout/api/src/main/res/values-it/translations.xml b/features/logout/api/src/main/res/values-it/translations.xml index 8b01a027800..7d1a3ae3046 100644 --- a/features/logout/api/src/main/res/values-it/translations.xml +++ b/features/logout/api/src/main/res/values-it/translations.xml @@ -3,6 +3,6 @@ "Sei sicuro di voler uscire?" "Esci" "Esci" - "Uscita in corso..." + "Uscita in corso…" "Esci" \ No newline at end of file diff --git a/features/rageshake/impl/src/main/res/values-es/translations.xml b/features/rageshake/impl/src/main/res/values-es/translations.xml index 527376f2680..0b1a374b979 100644 --- a/features/rageshake/impl/src/main/res/values-es/translations.xml +++ b/features/rageshake/impl/src/main/res/values-es/translations.xml @@ -4,7 +4,7 @@ "Podéis poneros en contacto conmigo para resolver dudas relacionadas" "Editar captura de pantalla" "Describe el problema. ¿Qué hiciste? ¿Qué esperabas que ocurriera? ¿Qué ocurrió en realidad? Por favor, detállalo todo lo que puedas." - "Describe el error..." + "Describe el error…" "Si es posible, escriba la descripción en inglés." "Enviar registros de fallos" "Enviar registros para ayudar" diff --git a/features/rageshake/impl/src/main/res/values-it/translations.xml b/features/rageshake/impl/src/main/res/values-it/translations.xml index 60397f87199..c8a15eeedfe 100644 --- a/features/rageshake/impl/src/main/res/values-it/translations.xml +++ b/features/rageshake/impl/src/main/res/values-it/translations.xml @@ -4,7 +4,7 @@ "Potete contattarmi per qualsiasi altra domanda" "Modifica istantanea schermo" "Descrivi il bug. Che cosa hai fatto? Cosa ti aspettavi che accadesse? Cosa è effettivamente accaduto. Si prega di inserire il maggior numero di dettagli possibile." - "Descrivi il problema..." + "Descrivi il problema…" "Se possibile, scrivere la descrizione in inglese." "Invia i log degli arresti anomali" "Invia i log per aiutarci" diff --git a/libraries/textcomposer/src/main/res/values-es/translations.xml b/libraries/textcomposer/src/main/res/values-es/translations.xml index 7392bd2b31a..e302765a584 100644 --- a/libraries/textcomposer/src/main/res/values-es/translations.xml +++ b/libraries/textcomposer/src/main/res/values-es/translations.xml @@ -2,7 +2,7 @@ "Lista de puntos" "Bloque de código" - "Mensaje..." + "Mensaje…" "Aplicar formato negrita" "Aplicar formato cursiva" "Aplicar formato tachado" diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml index d05dc300d2a..dbbf33933d8 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -49,7 +49,7 @@ "Acerca de" "Sonido" "Burbujas" - "Creando sala..." + "Creando sala…" "Saliste de la sala" "Error de descifrado" "Opciones de desarrollador" @@ -92,7 +92,7 @@ "Verificación cancelada" "Verificación completada" "Vídeo" - "Esperando..." + "Esperando…" "Confirmar" "Error" "Terminado" diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index 80fed2e6f83..96d0648d3b3 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -49,7 +49,7 @@ "Informazioni" "Audio" "Fumetti" - "Creazione stanza..." + "Creazione stanza…" "Hai lasciato la stanza" "Errore di decrittazione" "Opzioni sviluppatore" diff --git a/tools/localazy/generateLocalazyConfig.py b/tools/localazy/generateLocalazyConfig.py index 13b76e7d57c..c5f99aca2cf 100755 --- a/tools/localazy/generateLocalazyConfig.py +++ b/tools/localazy/generateLocalazyConfig.py @@ -20,6 +20,11 @@ def convertModuleToPath(name): ".*_ios" ] +# Replacement done in all string values +replacements = { + "...": "…" +} + # Store all regex specific to module, to eclude the corresponding keyx from the common string module allRegexToExcludeFromMainModule = [] # All actions that will be serialized in the localazy config @@ -33,6 +38,7 @@ def convertModuleToPath(name): "output": convertModuleToPath(entry["name"]) + "/src/main/res/values/localazy.xml", "includeKeys": list(map(lambda i: "REGEX:" + i, entry["includeRegex"])), "excludeKeys": list(map(lambda i: "REGEX:" + i, regexToAlwaysExclude)), + "replacements": replacements, "conditions": [ "equals: ${languageCode}, en" ] @@ -46,6 +52,7 @@ def convertModuleToPath(name): "output": convertModuleToPath(entry["name"]) + "/src/main/res/values-${langAndroidResNoScript}/translations.xml", "includeKeys": list(map(lambda i: "REGEX:" + i, entry["includeRegex"])), "excludeKeys": list(map(lambda i: "REGEX:" + i, regexToAlwaysExclude)), + "replacements": replacements, "conditions": [ "!equals: ${languageCode}, en" ] @@ -58,6 +65,7 @@ def convertModuleToPath(name): "type": "android", "output": "libraries/ui-strings/src/main/res/values/localazy.xml", "excludeKeys": list(map(lambda i: "REGEX:" + i, allRegexToExcludeFromMainModule + regexToAlwaysExclude)), + "replacements": replacements, "conditions": [ "equals: ${languageCode}, en" ] @@ -71,6 +79,7 @@ def convertModuleToPath(name): "type": "android", "output": "libraries/ui-strings/src/main/res/values-${langAndroidResNoScript}/translations.xml", "excludeKeys": list(map(lambda i: "REGEX:" + i, allRegexToExcludeFromMainModule + regexToAlwaysExclude)), + "replacements": replacements, "conditions": [ "!equals: ${languageCode}, en" ] From 19472807a65a068f74c4ada73d12c8fc9bc44d29 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 12:02:10 +0200 Subject: [PATCH 028/104] Use RetryDialog --- .../impl/root/CreateRoomRootView.kt | 8 +- .../components/dialogs/ErrorDialog.kt | 11 +-- .../components/dialogs/RetryDialog.kt | 97 +++++++++++++++++++ 3 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/RetryDialog.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index cc7fa853beb..146c4479967 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -44,7 +44,7 @@ import io.element.android.features.createroom.impl.R import io.element.android.features.userlist.api.UserListView import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.components.ProgressDialog -import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.components.dialogs.RetryDialog import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar @@ -109,12 +109,10 @@ fun CreateRoomRootView( ProgressDialog(text = stringResource(id = StringR.string.common_creating_room)) } is Async.Failure -> { - ErrorDialog( + RetryDialog( content = stringResource(id = StringR.string.screen_start_chat_error_starting_chat), - dismissText = stringResource(id = StringR.string.action_cancel), - submitText = stringResource(id = StringR.string.action_retry), onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) }, - onSubmit = { state.eventSink(CreateRoomRootEvents.RetryStartDM) }, + onRetry = { state.eventSink(CreateRoomRootEvents.RetryStartDM) }, ) } else -> Unit diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt index da1aeb836ec..ce1730ed0c3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt @@ -37,9 +37,7 @@ fun ErrorDialog( modifier: Modifier = Modifier, title: String = ErrorDialogDefaults.title, submitText: String = ErrorDialogDefaults.submitText, - dismissText: String? = null, onDismiss: () -> Unit = {}, - onSubmit: () -> Unit = onDismiss, shape: Shape = AlertDialogDefaults.shape, containerColor: Color = AlertDialogDefaults.containerColor, iconContentColor: Color = AlertDialogDefaults.iconContentColor, @@ -57,17 +55,10 @@ fun ErrorDialog( Text(content) }, confirmButton = { - TextButton(onClick = onSubmit) { + TextButton(onClick = onDismiss) { Text(submitText) } }, - dismissButton = dismissText?.let { - { - TextButton(onClick = onDismiss) { - Text(it) - } - } - }, shape = shape, containerColor = containerColor, iconContentColor = iconContentColor, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/RetryDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/RetryDialog.kt new file mode 100644 index 00000000000..ebfa8effc85 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/RetryDialog.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.components.dialogs + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.R as StringR + +@Composable +fun RetryDialog( + content: String, + modifier: Modifier = Modifier, + title: String = RetryDialogDefaults.title, + retryText: String = RetryDialogDefaults.retryText, + dismissText: String = RetryDialogDefaults.dismissText, + onRetry: () -> Unit = {}, + onDismiss: () -> Unit = {}, + shape: Shape = AlertDialogDefaults.shape, + containerColor: Color = AlertDialogDefaults.containerColor, + iconContentColor: Color = AlertDialogDefaults.iconContentColor, + titleContentColor: Color = AlertDialogDefaults.titleContentColor, + textContentColor: Color = AlertDialogDefaults.textContentColor, + tonalElevation: Dp = AlertDialogDefaults.TonalElevation, +) { + AlertDialog( + modifier = modifier, + onDismissRequest = onDismiss, + title = { + Text(title) + }, + text = { + Text(content) + }, + confirmButton = { + TextButton(onClick = onRetry) { + Text(retryText) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(dismissText) + } + }, + shape = shape, + containerColor = containerColor, + iconContentColor = iconContentColor, + titleContentColor = titleContentColor, + textContentColor = textContentColor, + tonalElevation = tonalElevation, + ) +} + +object RetryDialogDefaults { + val title: String @Composable get() = stringResource(id = StringR.string.dialog_title_error) + val retryText: String @Composable get() = stringResource(id = StringR.string.action_retry) + val dismissText: String @Composable get() = stringResource(id = StringR.string.action_cancel) +} + +@Preview +@Composable +internal fun RetryDialogLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun RetryDialogDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + RetryDialog( + content = "Content", + ) +} From 83a3379b2f6f8950a620bb9da2472e13811a8692 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 12:23:47 +0200 Subject: [PATCH 029/104] Create baseAction for shared values. --- tools/localazy/generateLocalazyConfig.py | 25 ++++++++++-------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/tools/localazy/generateLocalazyConfig.py b/tools/localazy/generateLocalazyConfig.py index c5f99aca2cf..e2bc310e1e6 100755 --- a/tools/localazy/generateLocalazyConfig.py +++ b/tools/localazy/generateLocalazyConfig.py @@ -20,9 +20,12 @@ def convertModuleToPath(name): ".*_ios" ] -# Replacement done in all string values -replacements = { - "...": "…" +baseAction = { + "type": "android", + # Replacement done in all string values + "replacements": { + "...": "…" + } } # Store all regex specific to module, to eclude the corresponding keyx from the common string module @@ -33,12 +36,10 @@ def convertModuleToPath(name): # Iterating on the config for entry in config["modules"]: # Create action for the default language - action = { - "type": "android", + action = baseAction | { "output": convertModuleToPath(entry["name"]) + "/src/main/res/values/localazy.xml", "includeKeys": list(map(lambda i: "REGEX:" + i, entry["includeRegex"])), "excludeKeys": list(map(lambda i: "REGEX:" + i, regexToAlwaysExclude)), - "replacements": replacements, "conditions": [ "equals: ${languageCode}, en" ] @@ -47,12 +48,10 @@ def convertModuleToPath(name): allActions.append(action) # Create action for the translations if allFiles: - actionTranslation = { - "type": "android", + actionTranslation = baseAction | { "output": convertModuleToPath(entry["name"]) + "/src/main/res/values-${langAndroidResNoScript}/translations.xml", "includeKeys": list(map(lambda i: "REGEX:" + i, entry["includeRegex"])), "excludeKeys": list(map(lambda i: "REGEX:" + i, regexToAlwaysExclude)), - "replacements": replacements, "conditions": [ "!equals: ${languageCode}, en" ] @@ -61,11 +60,9 @@ def convertModuleToPath(name): allRegexToExcludeFromMainModule.extend(entry["includeRegex"]) # Append configuration for the main string module: default language -mainAction = { - "type": "android", +mainAction = baseAction | { "output": "libraries/ui-strings/src/main/res/values/localazy.xml", "excludeKeys": list(map(lambda i: "REGEX:" + i, allRegexToExcludeFromMainModule + regexToAlwaysExclude)), - "replacements": replacements, "conditions": [ "equals: ${languageCode}, en" ] @@ -75,11 +72,9 @@ def convertModuleToPath(name): if allFiles: # Append configuration for the main string module: translations - mainActionTranslation = { - "type": "android", + mainActionTranslation = baseAction | { "output": "libraries/ui-strings/src/main/res/values-${langAndroidResNoScript}/translations.xml", "excludeKeys": list(map(lambda i: "REGEX:" + i, allRegexToExcludeFromMainModule + regexToAlwaysExclude)), - "replacements": replacements, "conditions": [ "!equals: ${languageCode}, en" ] From 3410cf4d4e3faca387b5651970458632a79ba059 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 12:25:17 +0200 Subject: [PATCH 030/104] Invoke `./tools/localazy/downloadStrings.sh --all` --- .../impl/src/main/res/values-es/translations.xml | 4 ---- .../ui-strings/src/main/res/values-es/translations.xml | 8 -------- 2 files changed, 12 deletions(-) diff --git a/features/roomdetails/impl/src/main/res/values-es/translations.xml b/features/roomdetails/impl/src/main/res/values-es/translations.xml index 817d3613ae8..ba4327000ba 100644 --- a/features/roomdetails/impl/src/main/res/values-es/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-es/translations.xml @@ -1,11 +1,7 @@ - "0 personas" "Una persona" - - - "%1$d personas" "Bloquear" diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml index dbbf33933d8..4b14f3a4a7d 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -116,19 +116,11 @@ "¿Seguro que quieres salir de la habitación?" "%1$s Android" - "No hay miembros" "%1$d miembro" - - - "%1$d miembros" - "No hay cambios en la sala" "%1$d cambio en la sala" - - - "%1$d cambios en la sala" "Agitar con fuerza para informar de un error" From b8bd02b663a391a35020526cd26a4292fb72ca04 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 13:29:00 +0200 Subject: [PATCH 031/104] screenshots tests --- ...aultGroup_RetryDialogDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ...ultGroup_RetryDialogLightPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 00000000000..62dcdf219cc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:596dee7cf5300dc5c2c6b314bf537619466f377b9946114e675c630e2e330976 +size 12059 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 00000000000..e1de911d46b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f791b1c552138b0d3184b936a51982cfc3e23c0b7de2c9ef5c31559d9279245 +size 12113 From d48f61b9898ef8e341672432917e418a7ba2d946 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 13:54:24 +0200 Subject: [PATCH 032/104] exclude fakes from code coverage --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index d228aa2acb0..0da25dc1bd5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -229,7 +229,7 @@ koverMerged { target = kotlinx.kover.api.VerificationTarget.CLASS overrideClassFilter { includes += "*Presenter" - excludes += "*TemplatePresenter" + excludes += "*Fake*Presenter" } bound { minValue = 90 From a108df2b143cd4ae4c6066015ea12a9dae2fd2e3 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 5 Apr 2023 15:36:41 +0200 Subject: [PATCH 033/104] [Room Details] Leave room (#296) * Add leave room functionality to the Room Details screen * Add snackbar message throught `SnackbarDistpacher` --- .../io/element/android/x/di/AppModule.kt | 7 + appnav/build.gradle.kts | 1 + .../android/appnav/LoggedInEventProcessor.kt | 72 +++++++++ .../android/appnav/LoggedInFlowNode.kt | 13 ++ .../io/element/android/appnav/RoomFlowNode.kt | 18 ++- changelog.d/286.feature | 1 + .../roomdetails/api/RoomDetailsEntryPoint.kt | 4 +- .../impl/DefaultRoomDetailsEntryPoint.kt | 5 +- .../roomdetails/impl/RoomDetailsEvent.kt | 6 +- .../roomdetails/impl/RoomDetailsPresenter.kt | 39 ++++- .../roomdetails/impl/RoomDetailsState.kt | 27 +++- .../impl/RoomDetailsStateProvider.kt | 4 +- .../roomdetails/impl/RoomDetailsView.kt | 46 +++++- .../roomdetails/RoomDetailsPresenterTests.kt | 138 +++++++++++++++++- .../features/roomlist/impl/RoomListEvents.kt | 1 - .../roomlist/impl/RoomListPresenter.kt | 19 ++- .../features/roomlist/impl/RoomListState.kt | 3 +- .../roomlist/impl/RoomListStateProvider.kt | 6 +- .../features/roomlist/impl/RoomListView.kt | 24 +-- .../roomlist/impl/RoomListPresenterTests.kt | 35 +---- .../impl/VerifySelfSessionPresenterTests.kt | 2 + .../components/dialogs/ConfirmationDialog.kt | 4 +- .../designsystem/utils/SnackbarDispatcher.kt | 72 +++++++++ .../libraries/matrix/api/MatrixClient.kt | 3 + .../libraries/matrix/api/room/MatrixRoom.kt | 4 + .../matrix/api/room/RoomMembershipObserver.kt | 40 +++++ .../libraries/matrix/impl/RustMatrixClient.kt | 8 +- .../matrix/impl/di/SessionMatrixModule.kt | 8 + .../matrix/impl/room/RustMatrixRoom.kt | 7 + .../libraries/matrix/test/FakeMatrixClient.kt | 5 + .../matrix/test/room/FakeMatrixRoom.kt | 9 ++ .../android/samples/minimal/RoomListScreen.kt | 4 +- 32 files changed, 564 insertions(+), 71 deletions(-) create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt create mode 100644 changelog.d/286.feature create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcher.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt diff --git a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt index c553f3f7f09..4a9ee85fc80 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt @@ -22,6 +22,7 @@ import dagger.Module import dagger.Provides import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn @@ -78,4 +79,10 @@ object AppModule { diffUpdateDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() ) } + + @Provides + @SingleIn(AppScope::class) + fun provideSnackbarDispatcher(): SnackbarDispatcher { + return SnackbarDispatcher() + } } diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index ea4c2d9a944..1cfcc405101 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.matrixui) + implementation(projects.libraries.uiStrings) implementation(projects.features.verifysession.api) implementation(projects.features.roomdetails.api) implementation(projects.tests.uitests) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt new file mode 100644 index 00000000000..f1d87330e15 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 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 io.element.android.appnav + +import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.SnackbarMessage +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.libraries.ui.strings.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.coroutines.coroutineContext + +class LoggedInEventProcessor @Inject constructor( + private val snackbarDispatcher: SnackbarDispatcher, + roomMembershipObserver: RoomMembershipObserver, + sessionVerificationService: SessionVerificationService, +) { + + private var observingJob: Job? = null + + private val displayLeftRoomMessage = roomMembershipObserver.updates + .map { !it.isUserInRoom } + + private val displayVerificationSuccessfulMessage = sessionVerificationService.verificationFlowState + .map { it == VerificationFlowState.Finished } + + fun observeEvents(coroutineScope: CoroutineScope) { + observingJob = coroutineScope.launch { + displayLeftRoomMessage.onEach { + displayMessage(R.string.common_current_user_left_room) + }.launchIn(this) + + displayVerificationSuccessfulMessage + .drop(1) + .onEach { + displayMessage(R.string.common_verification_complete) + }.launchIn(this) + } + } + + fun stopObserving() { + observingJob?.cancel() + observingJob = null + } + + private suspend fun displayMessage(message: Int) { + snackbarDispatcher.post(SnackbarMessage(message)) + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 4b2d653d792..1028c6f16dd 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -46,13 +46,17 @@ import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.MAIN_SPACE import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.di.MatrixUIBindings import io.element.android.services.appnavstate.api.AppNavigationStateService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.parcelize.Parcelize +import kotlin.coroutines.coroutineContext @ContributesNode(AppScope::class) class LoggedInFlowNode @AssistedInject constructor( @@ -63,6 +67,8 @@ class LoggedInFlowNode @AssistedInject constructor( private val createRoomEntryPoint: CreateRoomEntryPoint, private val appNavigationStateService: AppNavigationStateService, private val verifySessionEntryPoint: VerifySessionEntryPoint, + private val coroutineScope: CoroutineScope, + snackbarDispatcher: SnackbarDispatcher, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.RoomList, @@ -87,6 +93,11 @@ class LoggedInFlowNode @AssistedInject constructor( ) : NodeInputs private val inputs: Inputs = inputs() + private val loggedInFlowProcessor = LoggedInEventProcessor( + snackbarDispatcher, + inputs.matrixClient.roomMembershipObserver(), + inputs.matrixClient.sessionVerificationService(), + ) override fun onBuilt() { super.onBuilt() @@ -99,6 +110,7 @@ class LoggedInFlowNode @AssistedInject constructor( appNavigationStateService.onNavigateToSession(inputs.matrixClient.sessionId) // TODO We do not support Space yet, so directly navigate to main space appNavigationStateService.onNavigateToSpace(MAIN_SPACE) + loggedInFlowProcessor.observeEvents(coroutineScope) }, onDestroy = { val imageLoaderFactory = bindings().notLoggedInImageLoaderFactory() @@ -106,6 +118,7 @@ class LoggedInFlowNode @AssistedInject constructor( plugins().forEach { it.onFlowReleased(inputs.matrixClient) } appNavigationStateService.onLeavingSpace() appNavigationStateService.onLeavingSession() + loggedInFlowProcessor.stopObserving() } ) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt index 14b90b00643..69c02d500eb 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -18,6 +18,7 @@ package io.element.android.appnav import android.os.Parcelable import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.lifecycle.subscribe @@ -38,7 +39,12 @@ import io.element.android.libraries.architecture.animation.rememberDefaultTransi import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.services.appnavstate.api.AppNavigationStateService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize import timber.log.Timber @@ -49,6 +55,8 @@ class RoomFlowNode @AssistedInject constructor( private val messagesEntryPoint: MessagesEntryPoint, private val roomDetailsEntryPoint: RoomDetailsEntryPoint, private val appNavigationStateService: AppNavigationStateService, + roomMembershipObserver: RoomMembershipObserver, + coroutineScope: CoroutineScope, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.Messages, @@ -68,6 +76,7 @@ class RoomFlowNode @AssistedInject constructor( ) : NodeInputs private val inputs: Inputs = inputs() + private val timeline = inputs.room.timeline() private val roomFlowPresenter = RoomFlowPresenter(inputs.room) @@ -85,6 +94,13 @@ class RoomFlowNode @AssistedInject constructor( appNavigationStateService.onLeavingRoom() } ) + + roomMembershipObserver.updates + .filter { update -> update.roomId == inputs.room.roomId && !update.isUserInRoom } + .onEach { + navigateUp() + } + .launchIn(coroutineScope) } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -97,7 +113,7 @@ class RoomFlowNode @AssistedInject constructor( }) } NavTarget.RoomDetails -> { - roomDetailsEntryPoint.createNode(this, buildContext) + roomDetailsEntryPoint.createNode(this, buildContext, emptyList()) } } } diff --git a/changelog.d/286.feature b/changelog.d/286.feature new file mode 100644 index 00000000000..0a193367fd0 --- /dev/null +++ b/changelog.d/286.feature @@ -0,0 +1 @@ +Add leave room functionality to the Room Details screen. diff --git a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt index e56cc7e7052..560e9d5dd78 100644 --- a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt +++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt @@ -18,9 +18,9 @@ package io.element.android.features.roomdetails.api import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint interface RoomDetailsEntryPoint : FeatureEntryPoint { - fun createNode(parentNode: Node, buildContext: BuildContext): Node - + fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List): Node } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt index a52575606cc..eebdbea062a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt @@ -18,6 +18,7 @@ package io.element.android.features.roomdetails.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.libraries.architecture.createNode @@ -26,7 +27,7 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultRoomDetailsEntryPoint @Inject constructor() : RoomDetailsEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + override fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List): Node { + return parentNode.createNode(buildContext, plugins) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt index ad6b4ea78f9..3ef87d17e0d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt @@ -16,4 +16,8 @@ package io.element.android.features.roomdetails.impl -sealed interface RoomDetailsEvent +sealed interface RoomDetailsEvent { + data class LeaveRoom(val needsConfirmation: Boolean) : RoomDetailsEvent + object ClearLeaveRoomWarning : RoomDetailsEvent + object ClearError : RoomDetailsEvent +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 80ae4426d92..48dce4609c8 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -21,22 +21,31 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject class RoomDetailsPresenter @Inject constructor( private val room: MatrixRoom, + private val roomMembershipObserver: RoomMembershipObserver, ) : Presenter { @Composable override fun present(): RoomDetailsState { -// fun handleEvents(event: RoomDetailsEvent) {} - + val coroutineScope = rememberCoroutineScope() + var leaveRoomWarning by remember { + mutableStateOf(null) + } + var error by remember { + mutableStateOf(null) + } var memberCount: Async by remember { mutableStateOf(Async.Loading()) } LaunchedEffect(Unit) { withContext(Dispatchers.IO) { @@ -47,6 +56,28 @@ class RoomDetailsPresenter @Inject constructor( ) } } + fun handleEvents(event: RoomDetailsEvent) { + when (event) { + is RoomDetailsEvent.LeaveRoom -> { + if (event.needsConfirmation) { + leaveRoomWarning = LeaveRoomWarning.computeLeaveRoomWarning(room.isPublic, memberCount) + } else { + coroutineScope.launch(Dispatchers.IO) { + room.leave() + .onSuccess { + roomMembershipObserver.notifyUserLeftRoom(room.roomId) + }.onFailure { + error = RoomDetailsError.AlertGeneric + } + leaveRoomWarning = null + } + } + } + is RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning = null + RoomDetailsEvent.ClearError -> error = null + } + } + return RoomDetailsState( roomId = room.roomId.value, @@ -56,7 +87,9 @@ class RoomDetailsPresenter @Inject constructor( roomTopic = room.topic, memberCount = memberCount, isEncrypted = room.isEncrypted, -// eventSink = ::handleEvents + displayLeaveRoomWarning = leaveRoomWarning, + error = error, + eventSink = ::handleEvents ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index 78ee70529d7..a9d3dc2fb19 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -17,6 +17,9 @@ package io.element.android.features.roomdetails.impl import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.isLoading + +import io.element.android.libraries.matrix.api.room.MatrixRoom data class RoomDetailsState( val roomId: String, @@ -26,5 +29,27 @@ data class RoomDetailsState( val roomTopic: String?, val memberCount: Async, val isEncrypted: Boolean, -// val eventSink: (RoomDetailsEvent) -> Unit + val displayLeaveRoomWarning: LeaveRoomWarning?, + val error: RoomDetailsError?, + val eventSink: (RoomDetailsEvent) -> Unit ) + +sealed class LeaveRoomWarning { + object Generic : LeaveRoomWarning() + object PrivateRoom : LeaveRoomWarning() + object LastUserInRoom : LeaveRoomWarning() + + companion object { + fun computeLeaveRoomWarning(isPublic: Boolean, memberCount: Async): LeaveRoomWarning { + return when { + !isPublic -> PrivateRoom + (memberCount as? Async.Success)?.state == 1 -> LastUserInRoom + else -> Generic + } + } + } +} + +sealed interface RoomDetailsError { + object AlertGeneric : RoomDetailsError +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index c91db4d3a87..2629d76aed4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -43,5 +43,7 @@ fun aRoomDetailsState() = RoomDetailsState( "|| MAI iki/Marketing...", memberCount = Async.Success(32), isEncrypted = true, -// eventSink = {} + displayLeaveRoomWarning = null, + error = null, + eventSink = {} ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index e4d4ac56096..d5b2a4a9e4f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -49,6 +49,8 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -57,6 +59,7 @@ import io.element.android.libraries.designsystem.theme.LocalColors import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.ui.strings.R as StringR @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -101,7 +104,24 @@ fun RoomDetailsView( SecuritySection() } - OtherActionsSection() + OtherActionsSection(onLeaveRoom = { + state.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) + }) + + if (state.displayLeaveRoomWarning != null) { + ConfirmLeaveRoomDialog( + leaveRoomWarning = state.displayLeaveRoomWarning, + onConfirmLeave = { state.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) }, + onDismiss = { state.eventSink(RoomDetailsEvent.ClearLeaveRoomWarning) } + ) + } + + if (state.error != null) { + ErrorDialog( + content = stringResource(StringR.string.error_unknown), + onDismiss = { state.eventSink(RoomDetailsEvent.ClearError) } + ) + } } } } @@ -189,16 +209,38 @@ internal fun SecuritySection(modifier: Modifier = Modifier) { } @Composable -internal fun OtherActionsSection(modifier: Modifier = Modifier) { +internal fun OtherActionsSection(onLeaveRoom: () -> Unit, modifier: Modifier = Modifier) { PreferenceCategory(showDivider = false, modifier = modifier) { PreferenceText( title = stringResource(R.string.screen_room_details_leave_room_title), icon = ImageVector.vectorResource(R.drawable.ic_door_open), tintColor = LocalColors.current.textActionCritical, + onClick = onLeaveRoom, ) } } +@Composable +internal fun ConfirmLeaveRoomDialog( + leaveRoomWarning: LeaveRoomWarning, + onConfirmLeave: () -> Unit, + onDismiss: () -> Unit +) { + val content = stringResource( + when (leaveRoomWarning) { + LeaveRoomWarning.PrivateRoom -> StringR.string.leave_room_alert_private_subtitle + LeaveRoomWarning.LastUserInRoom -> StringR.string.leave_room_alert_empty_subtitle + LeaveRoomWarning.Generic -> StringR.string.leave_room_alert_subtitle + } + ) + ConfirmationDialog( + content = content, + submitText = stringResource(StringR.string.action_leave), + onSubmitClicked = onConfirmLeave, + onDismiss = onDismiss, + ) +} + @Preview @Composable fun RoomDetailsLightPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) = diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 7679e1eb7d4..41404b73549 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -20,21 +20,37 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth +import io.element.android.features.roomdetails.impl.LeaveRoomWarning +import io.element.android.features.roomdetails.impl.RoomDetailsEvent import io.element.android.features.roomdetails.impl.RoomDetailsPresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take import kotlinx.coroutines.test.runTest import org.junit.Test +@ExperimentalCoroutinesApi class RoomDetailsPresenterTests { + + private val roomMembershipObserver = RoomMembershipObserver(A_SESSION_ID) + @Test fun `present - initial state is created from room info`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(room) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -53,7 +69,7 @@ class RoomDetailsPresenterTests { @Test fun `present - room member count is calculated asynchronously`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(room) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -68,7 +84,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state with no room name`() = runTest { val room = aMatrixRoom(name = null) - val presenter = RoomDetailsPresenter(room) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -84,7 +100,7 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom(name = null).apply { givenFetchMemberResult(Result.failure(Throwable())) } - val presenter = RoomDetailsPresenter(room) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -94,6 +110,100 @@ class RoomDetailsPresenterTests { cancelAndIgnoreRemainingEvents() } } + + @Test + fun `present - Leave with confirmation on private room shows a specific warning`() = runTest { + val room = aMatrixRoom(isPublic = false) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // Allow room member count to load + skipItems(1) + + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) + val confirmationState = awaitItem() + Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.PrivateRoom) + } + } + + @Test + fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest { + val room = aMatrixRoom(members = listOf(aRoomMember())) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // Allow room member count to load + skipItems(1) + + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) + val confirmationState = awaitItem() + Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.LastUserInRoom) + } + } + + @Test + fun `present - Leave with confirmation shows a generic warning`() = runTest { + val room = aMatrixRoom() + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // Allow room member count to load + skipItems(1) + + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) + val confirmationState = awaitItem() + Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.Generic) + } + } + + @Test + fun `present - Leave without confirmation leaves the room`() = runTest { + val room = aMatrixRoom() + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // Allow room member count to load + skipItems(1) + + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) + + cancelAndIgnoreRemainingEvents() + } + + // Membership observer should receive a 'left room' change + roomMembershipObserver.updates.take(1) + .onEach { update -> Truth.assertThat(update.change).isEqualTo(MembershipChange.LEFT) } + .collect() + } + + @Test + fun `present - ClearError removes any error present`() = runTest { + val room = aMatrixRoom().apply { + givenLeaveRoomError(Throwable()) + } + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // Allow room member count to load + skipItems(1) + + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) + val errorState = awaitItem() + Truth.assertThat(errorState.error).isNotNull() + errorState.eventSink(RoomDetailsEvent.ClearError) + Truth.assertThat(awaitItem().error).isNull() + } + } } fun aMatrixRoom( @@ -104,6 +214,7 @@ fun aMatrixRoom( avatarUrl: String? = "https://matrix.org/avatar.jpg", members: List = emptyList(), isEncrypted: Boolean = true, + isPublic: Boolean = true, ) = FakeMatrixRoom( roomId = roomId, name = name, @@ -112,4 +223,23 @@ fun aMatrixRoom( avatarUrl = avatarUrl, members = members, isEncrypted = isEncrypted, + isPublic = isPublic, +) + +fun aRoomMember( + userId: UserId = A_USER_ID, + displayName: String? = null, + avatarUrl: String? = null, + membership: RoomMembershipState = RoomMembershipState.JOIN, + isNameAmbiguous: Boolean = false, + powerLevel: Long = 0L, + normalizedPowerLevel: Long = 0L +) = RoomMember( + userId = userId.value, + displayName = displayName, + avatarUrl = avatarUrl, + membership = membership, + isNameAmbiguous = isNameAmbiguous, + powerLevel = powerLevel, + normalizedPowerLevel = normalizedPowerLevel, ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt index 47c34d6a5c9..299c670eb49 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt @@ -20,5 +20,4 @@ sealed interface RoomListEvents { data class UpdateFilter(val newFilter: String) : RoomListEvents data class UpdateVisibleRange(val range: IntRange) : RoomListEvents object DismissRequestVerificationPrompt : RoomListEvents - object ClearSuccessfulVerificationMessage : RoomListEvents } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 7d9a62f2d73..ac37dfe30d3 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -34,12 +34,14 @@ import io.element.android.libraries.core.extensions.orEmpty import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.handleSnackbarMessage import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus -import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -56,8 +58,11 @@ class RoomListPresenter @Inject constructor( private val lastMessageTimestampFormatter: LastMessageTimestampFormatter, private val roomLastMessageFormatter: RoomLastMessageFormatter, private val sessionVerificationService: SessionVerificationService, + private val snackbarDispatcher: SnackbarDispatcher, ) : Presenter { + private val roomMembershipObserver: RoomMembershipObserver = client.roomMembershipObserver() + @Composable override fun present(): RoomListState { val matrixUser: MutableState = remember { @@ -86,19 +91,11 @@ class RoomListPresenter @Inject constructor( derivedStateOf { sessionVerifiedStatus == SessionVerifiedStatus.NotVerified && !verificationPromptDismissed } } - // Current verification flow status, if any (initial, requesting, accepted, etc.) - val currentVerificationFlowStatus by sessionVerificationService.verificationFlowState.collectAsState() - // We only care about the 'Finished' state to display the 'verification success' message - val presentVerificationSuccessfulMessage = remember { - derivedStateOf { currentVerificationFlowStatus == VerificationFlowState.Finished } - } - fun handleEvents(event: RoomListEvents) { when (event) { is RoomListEvents.UpdateFilter -> filter = event.newFilter is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range) RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true - RoomListEvents.ClearSuccessfulVerificationMessage -> sessionVerificationService.reset() } } @@ -106,12 +103,14 @@ class RoomListPresenter @Inject constructor( filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter) } + val snackbarMessage = handleSnackbarMessage(snackbarDispatcher) + return RoomListState( matrixUser = matrixUser.value, roomList = filteredRoomSummaries.value, filter = filter, - presentVerificationSuccessfulMessage = presentVerificationSuccessfulMessage.value, displayVerificationPrompt = displayVerificationPrompt, + snackbarMessage = snackbarMessage, eventSink = ::handleEvents ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index 122c5e55068..a14ef74e94e 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -18,6 +18,7 @@ package io.element.android.features.roomlist.impl import androidx.compose.runtime.Immutable import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList @@ -26,7 +27,7 @@ data class RoomListState( val matrixUser: MatrixUser?, val roomList: ImmutableList, val filter: String, - val presentVerificationSuccessfulMessage: Boolean, val displayVerificationPrompt: Boolean, + val snackbarMessage: SnackbarMessage?, val eventSink: (RoomListEvents) -> Unit ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 68db6316752..d1ca647d62c 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -20,17 +20,19 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import io.element.android.libraries.ui.strings.R as StringR open class RoomListStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomListState(), aRoomListState().copy(displayVerificationPrompt = true), - aRoomListState().copy(presentVerificationSuccessfulMessage = true), + aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)), ) } @@ -39,7 +41,7 @@ internal fun aRoomListState() = RoomListState( roomList = aRoomListRoomSummaryList(), filter = "filter", eventSink = {}, - presentVerificationSuccessfulMessage = false, + snackbarMessage = null, displayVerificationPrompt = false, ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index 8d31b17a141..1f49d8db0fd 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -40,10 +40,11 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -67,6 +68,7 @@ import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.coroutines.launch import io.element.android.libraries.designsystem.R as DrawableR import io.element.android.libraries.ui.strings.R as StringR @@ -130,14 +132,18 @@ fun RoomListContent( } val snackbarHostState = remember { SnackbarHostState() } - val verificationCompleteMessage = stringResource(StringR.string.common_verification_complete) - LaunchedEffect(state.presentVerificationSuccessfulMessage) { - if (state.presentVerificationSuccessfulMessage) { - snackbarHostState.showSnackbar( - message = verificationCompleteMessage, - duration = SnackbarDuration.Short, - ) - state.eventSink(RoomListEvents.ClearSuccessfulVerificationMessage) + val snackbarMessageText = if (state.snackbarMessage != null ) { + stringResource(state.snackbarMessage.messageResId) + } else null + val coroutineScope = rememberCoroutineScope() + if (snackbarMessageText != null) { + SideEffect { + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = snackbarMessageText, + duration = SnackbarDuration.Short, + ) + } } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index 1e8c15bf6d1..3f3e43e2e73 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -24,8 +24,8 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus -import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -49,6 +49,7 @@ class RoomListPresenterTests { createDateFormatter(), FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), + SnackbarDispatcher(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -75,6 +76,7 @@ class RoomListPresenterTests { createDateFormatter(), FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), + SnackbarDispatcher(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -95,6 +97,7 @@ class RoomListPresenterTests { createDateFormatter(), FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), + SnackbarDispatcher(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -119,6 +122,7 @@ class RoomListPresenterTests { createDateFormatter(), FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), + SnackbarDispatcher(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -148,6 +152,7 @@ class RoomListPresenterTests { createDateFormatter(), FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), + SnackbarDispatcher(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -182,6 +187,7 @@ class RoomListPresenterTests { createDateFormatter(), FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), + SnackbarDispatcher(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -230,6 +236,7 @@ class RoomListPresenterTests { givenIsReady(true) givenVerifiedStatus(SessionVerifiedStatus.NotVerified) }, + SnackbarDispatcher(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -242,32 +249,6 @@ class RoomListPresenterTests { } } - @Test - fun `present - presentVerificationSuccessfulMessage & ClearVerificationSuccesfulMessage`() = runTest { - val roomSummaryDataSource = FakeRoomSummaryDataSource() - val presenter = RoomListPresenter( - FakeMatrixClient( - sessionId = A_SESSION_ID, - roomSummaryDataSource = roomSummaryDataSource - ), - createDateFormatter(), - FakeRoomLastMessageFormatter(), - FakeSessionVerificationService().apply { - givenIsReady(true) - givenVerificationFlowState(VerificationFlowState.Finished) - }, - ) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - skipItems(1) - val displayMessageItem = awaitItem() - Truth.assertThat(displayMessageItem.presentVerificationSuccessfulMessage).isTrue() - displayMessageItem.eventSink(RoomListEvents.ClearSuccessfulVerificationMessage) - Truth.assertThat(awaitItem().presentVerificationSuccessfulMessage).isFalse() - } - } - private fun createDateFormatter(): LastMessageTimestampFormatter { return FakeLastMessageTimestampFormatter().apply { givenFormat(A_FORMATTED_DATE) diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt index 6c79c1b7626..4007f4c8cf3 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt @@ -27,9 +27,11 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test +@ExperimentalCoroutinesApi class VerifySelfSessionPresenterTests { @Test diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt index ff434c97488..07a85e06459 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt @@ -37,11 +37,11 @@ import io.element.android.libraries.ui.strings.R as StringR @Composable fun ConfirmationDialog( - title: String, content: String, onSubmitClicked: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, + title: String? = null, submitText: String = stringResource(id = StringR.string.action_ok), cancelText: String = stringResource(id = StringR.string.action_cancel), thirdButtonText: String? = null, @@ -60,7 +60,7 @@ fun ConfirmationDialog( modifier = modifier, onDismissRequest = onDismiss, title = { - Text(text = title) + if (title != null) { Text(text = title) } }, text = { Text(content) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcher.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcher.kt new file mode 100644 index 00000000000..1131777398b --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcher.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.utils + +import androidx.annotation.StringRes +import androidx.compose.material3.SnackbarDuration +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +class SnackbarDispatcher { + private val mutex = Mutex() + + private val snackbarState = MutableStateFlow(null) + val snackbarMessage: Flow = snackbarState + + suspend fun post(message: SnackbarMessage) { + mutex.withLock { + snackbarState.update { message } + } + } + + suspend fun clear() { + mutex.withLock { + snackbarState.update { null } + } + } +} + +@Composable +fun handleSnackbarMessage( + snackbarDispatcher: SnackbarDispatcher +): SnackbarMessage? { + val snackbarMessage by snackbarDispatcher.snackbarMessage.collectAsState(initial = null) + LaunchedEffect(snackbarMessage) { + if (snackbarMessage != null) { + launch(Dispatchers.Main) { + snackbarDispatcher.clear() + } + } + } + return snackbarMessage +} + +data class SnackbarMessage( + @StringRes val messageResId: Int, + val duration: SnackbarDuration = SnackbarDuration.Short, + @StringRes val actionResId: Int? = null, + val action: () -> Unit = {}, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 9ce55028439..8a991771a50 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.media.MediaResolver import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.verification.SessionVerificationService import java.io.Closeable @@ -43,4 +44,6 @@ interface MatrixClient : Closeable { ): Result fun onSlidingSyncUpdate() + + fun roomMembershipObserver(): RoomMembershipObserver } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index bd76d95b35a..60ff09a15b3 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -32,8 +32,10 @@ interface MatrixRoom: Closeable { val topic: String? val avatarUrl: String? val isEncrypted: Boolean + val isPublic: Boolean suspend fun members() : List + suspend fun memberCount(): Int fun syncUpdateFlow(): Flow @@ -53,4 +55,6 @@ interface MatrixRoom: Closeable { suspend fun replyMessage(eventId: EventId, message: String): Result suspend fun redactEvent(eventId: EventId, reason: String? = null): Result + + fun leave(): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt new file mode 100644 index 00000000000..42b0996bb16 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.matrix.api.room + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +class RoomMembershipObserver( + private val sessionId: SessionId, +) { + data class RoomMembershipUpdate( + val roomId: RoomId, + val isUserInRoom: Boolean, + val change: MembershipChange, + ) + + private val _updates = MutableSharedFlow(replay = 1) + val updates = _updates.asSharedFlow() + + fun notifyUserLeftRoom(roomId: RoomId) { + _updates.tryEmit(RoomMembershipUpdate(roomId, false, MembershipChange.LEFT)) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 378fec56c72..2fe34bfa556 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.impl.media.RustMediaResolver +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.impl.room.RustMatrixRoom import io.element.android.libraries.matrix.impl.room.RustRoomSummaryDataSource import io.element.android.libraries.matrix.impl.sync.SlidingSyncObserverProxy @@ -89,6 +90,7 @@ class RustMatrixClient constructor( requiredState = listOf( RequiredState(key = "m.room.avatar", value = ""), RequiredState(key = "m.room.encryption", value = ""), + RequiredState(key = "m.room.join_rules", value = ""), ) ) .filters(slidingSyncFilters) @@ -128,6 +130,8 @@ class RustMatrixClient constructor( private val mediaResolver = RustMediaResolver(this) private val isSyncing = AtomicBoolean(false) + private val roomMembershipObserver = RoomMembershipObserver(sessionId) + init { client.setDelegate(clientDelegate) rustRoomSummaryDataSource.init() @@ -150,7 +154,7 @@ class RustMatrixClient constructor( slidingSyncRoom = slidingSyncRoom, innerRoom = fullRoom, coroutineScope = coroutineScope, - coroutineDispatchers = dispatchers + coroutineDispatchers = dispatchers, ) } @@ -243,6 +247,8 @@ class RustMatrixClient constructor( } } + override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver + private fun File.deleteSessionDirectory(userID: String): Boolean { // Rust sanitises the user ID replacing invalid characters with an _ val sanitisedUserID = userID.replace(":", "_") diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt index 3079c75dc82..b49050cdc23 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt @@ -22,7 +22,9 @@ import dagger.Provides import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @Module @ContributesTo(SessionScope::class) @@ -32,4 +34,10 @@ object SessionMatrixModule { fun providesRustSessionVerificationService(matrixClient: MatrixClient): SessionVerificationService { return matrixClient.sessionVerificationService() } + + @Provides + @SingleIn(SessionScope::class) + fun provideRoomMembershipObserver(matrixClient: MatrixClient): RoomMembershipObserver { + return matrixClient.roomMembershipObserver() + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 291aa573921..fd5a63cb3b8 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -129,6 +129,9 @@ class RustMatrixRoom( override val alternativeAliases: List get() = innerRoom.alternativeAliases() + override val isPublic: Boolean + get() = innerRoom.isPublic() + override suspend fun fetchMembers(): Result = withContext(coroutineDispatchers.io) { runCatching { innerRoom.fetchMembers() @@ -179,4 +182,8 @@ class RustMatrixRoom( innerRoom.redact(eventId.value, reason, transactionId) } } + + override fun leave(): Result { + return runCatching { innerRoom.leave() } + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 9272e794b4a..998c7cdea40 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.media.MediaResolver import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.test.media.FakeMediaResolver @@ -81,4 +82,8 @@ class FakeMatrixClient( override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService override fun onSlidingSyncUpdate() {} + + override fun roomMembershipObserver(): RoomMembershipObserver { + return RoomMembershipObserver(A_SESSION_ID) + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index b8ea695f472..1c6d580a3c4 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -37,6 +37,7 @@ class FakeMatrixRoom( override val isEncrypted: Boolean = false, override val alias: String? = null, override val alternativeAliases: List = emptyList(), + override val isPublic: Boolean = true, private val members: List = emptyList(), private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), ) : MatrixRoom { @@ -46,6 +47,8 @@ class FakeMatrixRoom( var areMembersFetched: Boolean = false private set + private var leaveRoomError: Throwable? = null + override fun syncUpdateFlow(): Flow { return emptyFlow() } @@ -114,8 +117,14 @@ class FakeMatrixRoom( return Result.success(Unit) } + override fun leave(): Result = leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit) + override fun close() = Unit + fun givenLeaveRoomError(throwable: Throwable?) { + this.leaveRoomError = throwable + } + fun givenFetchMemberResult(result: Result) { fetchMemberResult = result } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 6c95dee2102..5dce2feafe3 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -26,6 +26,7 @@ import io.element.android.features.roomlist.impl.RoomListView import io.element.android.libraries.dateformatter.impl.DateFormatters import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider +import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.coroutines.launch @@ -47,7 +48,8 @@ class RoomListScreen( matrixClient, DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters), DefaultRoomLastMessageFormatter(context, matrixClient), - sessionVerificationService + sessionVerificationService, + SnackbarDispatcher(), ) @Composable From 8544ff549163d9bbcfe20a853106a0259aa7dea5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 13:56:35 +0000 Subject: [PATCH 034/104] Update peter-evans/create-pull-request action to v5 --- .github/workflows/sync-localazy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index 3efbb41f274..e3c745b8f6d 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -18,7 +18,7 @@ jobs: - name: Run Localazy script run: ./tools/localazy/downloadStrings.sh --all - name: Create Pull Request for Strings - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@v5 with: commit-message: Sync Strings from Localazy title: Sync Strings From e838bcff3bc7eab96370fe1988564ded697ea353 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 13 Mar 2023 14:37:25 +0100 Subject: [PATCH 035/104] Setup Google services Gradle plugin. --- app/build.gradle.kts | 1 + build.gradle.kts | 1 + 2 files changed, 2 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1e3de32f225..316f8ab5073 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,6 +33,7 @@ plugins { id("com.google.firebase.appdistribution") version "4.0.0" id("org.jetbrains.kotlinx.knit") version "0.4.0" id("kotlin-parcelize") + id("com.google.gms.google-services") } android { diff --git a/build.gradle.kts b/build.gradle.kts index d228aa2acb0..38a11235dfc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.cli.common.toBooleanLenient buildscript { dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10") + classpath("com.google.gms:google-services:4.3.15") } } From 0054f6b641d8431845ee15ed394c5b19575c5f43 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 13 Mar 2023 15:17:44 +0100 Subject: [PATCH 036/104] Configure com.google.firebase:firebase-bom and add dependency on `firebase-messaging-ktx` --- app/build.gradle.kts | 3 +++ gradle/libs.versions.toml | 4 ++-- plugins/build.gradle.kts | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 316f8ab5073..fdf0fcf0a3c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -223,6 +223,9 @@ dependencies { implementation(platform(libs.network.okhttp.bom)) implementation("com.squareup.okhttp3:logging-interceptor") + implementation(platform(libs.google.firebase.bom)) + implementation("com.google.firebase:firebase-messaging-ktx") + implementation(libs.dagger) kapt(libs.dagger.compiler) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 80f67bab6bd..8cce1df9e67 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,6 @@ [versions] # Project android_gradle_plugin = "7.4.2" -firebase_gradle_plugin = "3.2.0" kotlin = "1.8.10" ksp = "1.8.10-1.0.9" molecule = "0.8.0" @@ -55,8 +54,9 @@ dependencygraph = "0.10" [libraries] # Project android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" } -firebase_gradle_plugin = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebase_gradle_plugin" } kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +# https://firebase.google.com/docs/android/setup#available-libraries +google_firebase_bom = "com.google.firebase:firebase-bom:31.2.3" # AndroidX androidx_material = { module = "com.google.android.material:material", version.ref = "material" } diff --git a/plugins/build.gradle.kts b/plugins/build.gradle.kts index 6c77f11ac2d..d4324432f3d 100644 --- a/plugins/build.gradle.kts +++ b/plugins/build.gradle.kts @@ -27,6 +27,8 @@ repositories { dependencies { implementation(libs.android.gradle.plugin) implementation(libs.kotlin.gradle.plugin) - implementation(libs.firebase.gradle.plugin) + implementation(platform(libs.google.firebase.bom)) + // FIXME: using the bom ^, it should not be necessary to provide the version v... + implementation("com.google.firebase:firebase-appdistribution-gradle:4.0.0") implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } From 9a12b616ed9793c54625d15f0424188c0c4b72c3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 13 Mar 2023 15:21:03 +0100 Subject: [PATCH 037/104] Add google-services.json files to the project. --- app/src/debug/google-services.json | 49 ++++++++++++++++++++++++++++ app/src/nightly/google-services.json | 2 +- app/src/release/google-services.json | 40 +++++++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 app/src/debug/google-services.json create mode 100644 app/src/release/google-services.json diff --git a/app/src/debug/google-services.json b/app/src/debug/google-services.json new file mode 100644 index 00000000000..d9aa72f7baf --- /dev/null +++ b/app/src/debug/google-services.json @@ -0,0 +1,49 @@ +{ + "project_info": { + "project_number": "912726360885", + "firebase_url": "https://vector-alpha.firebaseio.com", + "project_id": "vector-alpha", + "storage_bucket": "vector-alpha.appspot.com" + }, + + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:912726360885:android:def0a4e454042e9b00427c", + "android_client_info": { + "package_name": "io.element.android.x.debug" + } + }, + "oauth_client": [ + { + "client_id": "912726360885-hvgoj23p6plt7hikhtdrakihojghaftv.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.element.android.x.debug", + "certificate_hash": "41bd63b3b612a15d9ba36a5245c393f2a9b992d1" + } + }, + { + "client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} diff --git a/app/src/nightly/google-services.json b/app/src/nightly/google-services.json index 09bfee08f7c..31b022b3f26 100644 --- a/app/src/nightly/google-services.json +++ b/app/src/nightly/google-services.json @@ -37,4 +37,4 @@ } ], "configuration_version": "1" -} \ No newline at end of file +} diff --git a/app/src/release/google-services.json b/app/src/release/google-services.json new file mode 100644 index 00000000000..16fd1e855cb --- /dev/null +++ b/app/src/release/google-services.json @@ -0,0 +1,40 @@ +{ + "project_info": { + "project_number": "912726360885", + "firebase_url": "https://vector-alpha.firebaseio.com", + "project_id": "vector-alpha", + "storage_bucket": "vector-alpha.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:912726360885:android:d097de99a4c23d2700427c", + "android_client_info": { + "package_name": "io.element.android.x" + } + }, + "oauth_client": [ + { + "client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} From 92987a4a0efd2b3b08579b8010513f5a686e1e4a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 14 Mar 2023 09:30:00 +0100 Subject: [PATCH 038/104] Add a link to a video presenting Anvil. --- docs/_developer_onboarding.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md index 9b07f37dafe..5b43b009227 100644 --- a/docs/_developer_onboarding.md +++ b/docs/_developer_onboarding.md @@ -251,7 +251,8 @@ Main libraries and frameworks used in this application: - Navigation state with [Appyx](https://bumble-tech.github.io/appyx/). Please watch [this video](https://www.droidcon.com/2022/11/15/model-driven-navigation-with-appyx-from-zero-to-hero/) to learn more about Appyx! -- DI: [Dagger](https://dagger.dev/) and [Anvil](https://github.com/square/anvil) +- DI: [Dagger](https://dagger.dev/) and [Anvil](https://github.com/square/anvil). Please + watch [this video](https://www.droidcon.com/2022/06/28/dagger-anvil-learning-to-love-dependency-injection/) to learn more about Anvil! - Reactive State management with Compose runtime and [Molecule](https://github.com/cashapp/molecule) Some patterns are inspired by [Circuit](https://slackhq.github.io/circuit/) From 50d12aca8af5f70e22d43724e933f22eda4d848d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 14 Mar 2023 14:57:14 +0100 Subject: [PATCH 039/104] Import some stuff about Push and notification from Element Android - WIP --- app/build.gradle.kts | 2 + .../io/element/android/x/di/AppModule.kt | 16 + gradle/libs.versions.toml | 6 +- .../libraries/core/cache/CircularCache.kt | 41 + .../libraries/di/DefaultPreferences.kt | 21 + libraries/push/api/build.gradle.kts | 28 + .../push/api/src/main/AndroidManifest.xml | 64 ++ .../android/libraries/push/api/PushService.kt | 23 + .../push/api/model/BackgroundSyncMode.kt | 48 ++ .../libraries/push/api/store/PushDataStore.kt | 40 + libraries/push/impl/build.gradle.kts | 64 ++ .../push/impl/src/main/AndroidManifest.xml | 64 ++ .../libraries/push/impl/AutoAcceptInvites.kt | 49 ++ .../libraries/push/impl/DefaultPushService.kt | 40 + .../impl/EnsureFcmTokenIsRetrievedUseCase.kt | 44 ++ .../android/libraries/push/impl/FcmHelper.kt | 49 ++ .../libraries/push/impl/GoogleFcmHelper.kt | 101 +++ .../push/impl/GuardServiceStarter.kt | 31 + .../push/impl/KeepInternalDistributor.kt | 30 + .../libraries/push/impl/PushersManager.kt | 124 +++ .../push/impl/RegisterUnifiedPushUseCase.kt | 68 ++ .../libraries/push/impl/UnifiedPushHelper.kt | 180 +++++ .../libraries/push/impl/UnifiedPushStore.kt | 77 ++ .../push/impl/UnregisterUnifiedPushUseCase.kt | 49 ++ .../impl/VectorFirebaseMessagingService.kt | 64 ++ .../libraries/push/impl/VectorPushHandler.kt | 188 +++++ .../VectorUnifiedPushMessagingReceiver.kt | 117 +++ .../libraries/push/impl/config/PushConfig.kt | 41 + .../di/FirebaseMessagingServiceBindings.kt | 26 + ...torUnifiedPushMessagingReceiverBindings.kt | 26 + .../libraries/push/impl/model/PushData.kt | 30 + .../libraries/push/impl/model/PushDataFcm.kt | 43 + .../push/impl/model/PushDataUnifiedPush.kt | 60 ++ .../notifications/FilteredEventDetector.kt | 57 ++ .../notifications/NotifiableEventProcessor.kt | 62 ++ .../notifications/NotifiableEventResolver.kt | 264 +++++++ .../impl/notifications/NotificationAction.kt | 39 + .../notifications/NotificationActionIds.kt | 41 + .../notifications/NotificationBitmapLoader.kt | 95 +++ .../NotificationBroadcastReceiver.kt | 247 ++++++ .../NotificationBroadcastReceiverBindings.kt | 25 + .../notifications/NotificationDisplayer.kt | 48 ++ .../NotificationDrawerManager.kt | 241 ++++++ .../NotificationEventPersistence.kt | 76 ++ .../notifications/NotificationEventQueue.kt | 152 ++++ .../impl/notifications/NotificationFactory.kt | 138 ++++ .../notifications/NotificationRenderer.kt | 133 ++++ .../impl/notifications/NotificationState.kt | 62 ++ .../impl/notifications/NotificationUtils.kt | 744 ++++++++++++++++++ .../notifications/OutdatedEventDetector.kt | 44 ++ .../push/impl/notifications/ProcessedEvent.kt | 31 + .../impl/notifications/RoomEventGroupInfo.kt | 35 + .../notifications/RoomGroupMessageCreator.kt | 166 ++++ .../SummaryGroupMessageCreator.kt | 157 ++++ .../notifications/TestNotificationReceiver.kt | 30 + .../model/InviteNotifiableEvent.kt | 33 + .../notifications/model/NotifiableEvent.kt | 31 + .../model/NotifiableMessageEvent.kt | 60 ++ .../model/SimpleNotifiableEvent.kt | 31 + .../libraries/push/impl/parser/PushParser.kt | 56 ++ .../push/impl/store/DefaultPushDataStore.kt | 133 ++++ .../drawable-xxhdpi/element_logo_green.xml | 22 + .../ic_material_done_all_white.png | Bin 0 -> 398 bytes .../res/drawable-xxhdpi/ic_notification.png | Bin 0 -> 1269 bytes .../vector_notification_accept_invitation.png | Bin 0 -> 473 bytes .../vector_notification_quick_reply.png | Bin 0 -> 269 bytes .../vector_notification_reject_invitation.png | Bin 0 -> 309 bytes .../push/impl/src/main/res/values/colors.xml | 22 + .../push/impl/src/main/res/values/dimens.xml | 21 + settings.gradle.kts | 10 + 70 files changed, 5158 insertions(+), 2 deletions(-) create mode 100644 libraries/core/src/main/kotlin/io/element/android/libraries/core/cache/CircularCache.kt create mode 100644 libraries/di/src/main/kotlin/io/element/android/libraries/di/DefaultPreferences.kt create mode 100644 libraries/push/api/build.gradle.kts create mode 100644 libraries/push/api/src/main/AndroidManifest.xml create mode 100644 libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt create mode 100644 libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/model/BackgroundSyncMode.kt create mode 100644 libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt create mode 100644 libraries/push/impl/build.gradle.kts create mode 100644 libraries/push/impl/src/main/AndroidManifest.xml create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/AutoAcceptInvites.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/EnsureFcmTokenIsRetrievedUseCase.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/FcmHelper.kt create mode 100755 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GuardServiceStarter.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/KeepInternalDistributor.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/RegisterUnifiedPushUseCase.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnregisterUnifiedPushUseCase.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorUnifiedPushMessagingReceiver.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/config/PushConfig.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/FirebaseMessagingServiceBindings.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/VectorUnifiedPushMessagingReceiverBindings.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataFcm.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataUnifiedPush.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FilteredEventDetector.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationAction.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverBindings.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt create mode 100755 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/OutdatedEventDetector.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ProcessedEvent.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiver.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/InviteNotifiableEvent.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/parser/PushParser.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt create mode 100644 libraries/push/impl/src/main/res/drawable-xxhdpi/element_logo_green.xml create mode 100755 libraries/push/impl/src/main/res/drawable-xxhdpi/ic_material_done_all_white.png create mode 100644 libraries/push/impl/src/main/res/drawable-xxhdpi/ic_notification.png create mode 100755 libraries/push/impl/src/main/res/drawable-xxhdpi/vector_notification_accept_invitation.png create mode 100755 libraries/push/impl/src/main/res/drawable-xxhdpi/vector_notification_quick_reply.png create mode 100755 libraries/push/impl/src/main/res/drawable-xxhdpi/vector_notification_reject_invitation.png create mode 100644 libraries/push/impl/src/main/res/values/colors.xml create mode 100644 libraries/push/impl/src/main/res/values/dimens.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fdf0fcf0a3c..0b8833efbba 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -214,10 +214,12 @@ dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") implementation(libs.appyx.core) implementation(libs.androidx.splash) + implementation(libs.androidx.core) implementation(libs.androidx.corektx) implementation(libs.androidx.lifecycle.runtime) implementation(libs.androidx.activity.compose) implementation(libs.androidx.startup) + implementation(libs.androidx.preference) implementation(libs.coil) implementation(platform(libs.network.okhttp.bom)) diff --git a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt index 4a9ee85fc80..d12d139a73d 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt @@ -17,6 +17,9 @@ package io.element.android.x.di import android.content.Context +import android.content.SharedPreferences +import android.content.res.Resources +import androidx.preference.PreferenceManager import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides @@ -25,6 +28,7 @@ import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.DefaultPreferences import io.element.android.libraries.di.SingleIn import io.element.android.x.BuildConfig import io.element.android.x.R @@ -47,6 +51,11 @@ object AppModule { return File(context.filesDir, "sessions") } + @Provides + fun providesResources(@ApplicationContext context: Context): Resources { + return context.resources + } + @Provides @SingleIn(AppScope::class) fun providesAppCoroutineScope(): CoroutineScope { @@ -69,6 +78,13 @@ object AppModule { okHttpLoggingLevel = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.BASIC, ) + @Provides + @SingleIn(AppScope::class) + @DefaultPreferences + fun providesDefaultSharedPreferences(@ApplicationContext context: Context): SharedPreferences { + return PreferenceManager.getDefaultSharedPreferences(context) + } + @Provides @SingleIn(AppScope::class) fun providesCoroutineDispatchers(): CoroutineDispatchers { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8cce1df9e67..e2fc6db401c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ molecule = "0.8.0" # AndroidX material = "1.8.0" -corektx = "1.9.0" +core = "1.9.0" datastore = "1.0.0" constraintlayout = "2.1.4" recyclerview = "1.3.0" @@ -60,7 +60,8 @@ google_firebase_bom = "com.google.firebase:firebase-bom:31.2.3" # AndroidX androidx_material = { module = "com.google.android.material:material", version.ref = "material" } -androidx_corektx = { module = "androidx.core:core-ktx", version.ref = "corektx" } +androidx_core = { module = "androidx.core:core", version.ref = "core" } +androidx_corektx = { module = "androidx.core:core-ktx", version.ref = "core" } androidx_datastore_preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } androidx_datastore_datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } androidx_constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } @@ -73,6 +74,7 @@ androidx_security_crypto = "androidx.security:security-crypto:1.0.0" androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" } androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" } androidx_startup = { module = "androidx.startup:startup-runtime", version.ref = "startup" } +androidx_preference = "androidx.preference:preference:1.2.0" androidx_compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" } diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/cache/CircularCache.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/cache/CircularCache.kt new file mode 100644 index 00000000000..f5305f006b8 --- /dev/null +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/cache/CircularCache.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.core.cache + +/** + * A FIFO circular buffer of T. + * This class is not thread safe. + */ +class CircularCache(cacheSize: Int, factory: (Int) -> Array) { + + companion object { + inline fun create(cacheSize: Int) = CircularCache(cacheSize) { Array(cacheSize) { null } } + } + + private val cache = factory(cacheSize) + private var writeIndex = 0 + + fun contains(value: T): Boolean = cache.contains(value) + + fun put(value: T) { + if (writeIndex == cache.size) { + writeIndex = 0 + } + cache[writeIndex] = value + writeIndex++ + } +} diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/DefaultPreferences.kt b/libraries/di/src/main/kotlin/io/element/android/libraries/di/DefaultPreferences.kt new file mode 100644 index 00000000000..2a4f9b8ac1e --- /dev/null +++ b/libraries/di/src/main/kotlin/io/element/android/libraries/di/DefaultPreferences.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 io.element.android.libraries.di + +import javax.inject.Qualifier + +@Qualifier annotation class DefaultPreferences diff --git a/libraries/push/api/build.gradle.kts b/libraries/push/api/build.gradle.kts new file mode 100644 index 00000000000..27a68273646 --- /dev/null +++ b/libraries/push/api/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.push.api" +} + +dependencies { + implementation(libs.androidx.corektx) + implementation(libs.coroutines.core) +} diff --git a/libraries/push/api/src/main/AndroidManifest.xml b/libraries/push/api/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..1d6f459d918 --- /dev/null +++ b/libraries/push/api/src/main/AndroidManifest.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt new file mode 100644 index 00000000000..3ed22d7dae3 --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.api + +interface PushService { + fun setCurrentRoom(roomId: String?) + fun setCurrentThread(threadId: String?) + fun notificationStyleChanged() +} diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/model/BackgroundSyncMode.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/model/BackgroundSyncMode.kt new file mode 100644 index 00000000000..3fb4841aba7 --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/model/BackgroundSyncMode.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.api.model + +/** + * Different strategies for Background sync, only applicable to F-Droid version of the app. + */ +enum class BackgroundSyncMode { + /** + * In this mode background syncs are scheduled via Workers, meaning that the system will have control on the periodicity + * of syncs when battery is low or when the phone is idle (sync will occur in allowed maintenance windows). After completion + * the sync work will schedule another one. + */ + FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY, + + /** + * This mode requires the app to be exempted from battery optimization. Alarms will be launched and will wake up the app + * in order to perform the background sync as a foreground service. After completion the service will schedule another alarm + */ + FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME, + + /** + * The app won't sync in background. + */ + FDROID_BACKGROUND_SYNC_MODE_DISABLED; + + companion object { + const val DEFAULT_SYNC_DELAY_SECONDS = 60 + const val DEFAULT_SYNC_TIMEOUT_SECONDS = 6 + + fun fromString(value: String?): BackgroundSyncMode = values().firstOrNull { it.name == value } + ?: FDROID_BACKGROUND_SYNC_MODE_DISABLED + } +} diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt new file mode 100644 index 00000000000..823ea0c88ca --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.api.store + +import io.element.android.libraries.push.api.model.BackgroundSyncMode +import kotlinx.coroutines.flow.Flow + +interface PushDataStore { + val pushCounterFlow: Flow + + fun areNotificationEnabledForDevice(): Boolean + fun setNotificationEnabledForDevice(enabled: Boolean) + + fun backgroundSyncTimeOut(): Int + fun setBackgroundSyncTimeout(timeInSecond: Int) + fun backgroundSyncDelay(): Int + fun setBackgroundSyncDelay(timeInSecond: Int) + fun isBackgroundSyncEnabled(): Boolean + fun setFdroidSyncBackgroundMode(mode: BackgroundSyncMode) + fun getFdroidSyncBackgroundMode(): BackgroundSyncMode + + /** + * Return true if Pin code is disabled, or if user set the settings to see full notification content. + */ + fun useCompleteNotificationFormat(): Boolean +} diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts new file mode 100644 index 00000000000..11950ad5f1a --- /dev/null +++ b/libraries/push/impl/build.gradle.kts @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 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. + */ + +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) + kotlin("plugin.serialization") version "1.8.10" +} + +android { + namespace = "io.element.android.libraries.push.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.dagger) + implementation(libs.androidx.corektx) + implementation(libs.androidx.datastore.preferences) + implementation(libs.androidx.lifecycle.process) + implementation(libs.serialization.json) + + implementation(projects.libraries.architecture) + implementation(projects.libraries.analytics.api) + implementation(projects.libraries.uiStrings) + implementation(projects.libraries.core) + implementation(projects.libraries.di) + implementation(projects.libraries.androidutils) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.push.api) + + implementation(projects.services.toolbox.api) + + + api("me.gujun.android:span:1.7") { + exclude(group = "com.android.support", module = "support-annotations") + } + + implementation(platform(libs.google.firebase.bom)) + implementation("com.google.firebase:firebase-messaging-ktx") + + // UnifiedPush + api("com.github.UnifiedPush:android-connector:2.1.1") + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(libs.coroutines.test) +} diff --git a/libraries/push/impl/src/main/AndroidManifest.xml b/libraries/push/impl/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..1d6f459d918 --- /dev/null +++ b/libraries/push/impl/src/main/AndroidManifest.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/AutoAcceptInvites.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/AutoAcceptInvites.kt new file mode 100644 index 00000000000..cc2b9100ece --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/AutoAcceptInvites.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +// TODO Move away +/** + * This interface defines 2 flags so you can handle auto accept invites. + * At the moment we only have [CompileTimeAutoAcceptInvites] implementation. + */ +interface AutoAcceptInvites { + /** + * Enable auto-accept invites. It means, as soon as you got an invite from the sync, it will try to join it. + */ + val isEnabled: Boolean + + /** + * Hide invites from the UI (from notifications, notification count and room list). By default invites are hidden when [isEnabled] is true + */ + val hideInvites: Boolean + get() = isEnabled +} + +fun AutoAcceptInvites.showInvites() = !hideInvites + +/** + * Simple compile time implementation of AutoAcceptInvites flags. + */ +@ContributesBinding(AppScope::class) +class CompileTimeAutoAcceptInvites @Inject constructor() : AutoAcceptInvites { + override val isEnabled = false +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt new file mode 100644 index 00000000000..cf7a5f377b3 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.api.PushService +import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultPushService @Inject constructor( + private val notificationDrawerManager: NotificationDrawerManager, +) : PushService { + override fun setCurrentRoom(roomId: String?) { + notificationDrawerManager.setCurrentRoom(roomId) + } + + override fun setCurrentThread(threadId: String?) { + notificationDrawerManager.setCurrentThread(threadId) + } + + override fun notificationStyleChanged() { + notificationDrawerManager.notificationStyleChanged() + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/EnsureFcmTokenIsRetrievedUseCase.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/EnsureFcmTokenIsRetrievedUseCase.kt new file mode 100644 index 00000000000..fa5e6a0e5d7 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/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 io.element.android.libraries.push.impl + +import javax.inject.Inject + +class EnsureFcmTokenIsRetrievedUseCase @Inject constructor( + private val unifiedPushHelper: UnifiedPushHelper, + private val fcmHelper: FcmHelper, + // private val activeSessionHolder: ActiveSessionHolder, +) { + + fun execute(pushersManager: PushersManager, registerPusher: Boolean) { + if (unifiedPushHelper.isEmbeddedDistributor()) { + fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher)) + } + } + + private fun shouldAddHttpPusher(registerPusher: Boolean) = if (registerPusher) { + /* + TODO EAx + val currentSession = activeSessionHolder.getActiveSession() + val currentPushers = currentSession.pushersService().getPushers() + currentPushers.none { it.deviceId == currentSession.sessionParams.deviceId } + */ + true + } else { + false + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/FcmHelper.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/FcmHelper.kt new file mode 100644 index 00000000000..9b8b6c2281f --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/FcmHelper.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 io.element.android.libraries.push.impl + +interface FcmHelper { + fun isFirebaseAvailable(): Boolean + + /** + * Retrieves the FCM registration token. + * + * @return the FCM token or null if not received from FCM. + */ + fun getFcmToken(): String? + + /** + * Store FCM token to the SharedPrefs. + * + * @param token the token to store. + */ + fun storeFcmToken(token: String?) + + /** + * onNewToken may not be called on application upgrade, so ensure my shared pref is set. + * + * @param pushersManager the instance to register the pusher on. + * @param registerPusher whether the pusher should be registered. + */ + fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) + + /* + fun onEnterForeground(activeSessionHolder: ActiveSessionHolder) + + fun onEnterBackground(activeSessionHolder: ActiveSessionHolder) + */ +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt new file mode 100755 index 00000000000..3d602aeb9bd --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2018 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 io.element.android.libraries.push.impl + +import android.content.Context +import android.content.SharedPreferences +import android.widget.Toast +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 com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.DefaultPreferences +import timber.log.Timber +import javax.inject.Inject +import io.element.android.libraries.ui.strings.R as StringR + +/** + * This class store the FCM token in SharedPrefs and ensure this token is retrieved. + * It has an alter ego in the fdroid variant. + */ +@ContributesBinding(AppScope::class) +class GoogleFcmHelper @Inject constructor( + @ApplicationContext private val context: Context, + @DefaultPreferences private val sharedPrefs: SharedPreferences, +) : FcmHelper { + override fun isFirebaseAvailable(): Boolean = true + + override fun getFcmToken(): String? { + return sharedPrefs.getString(PREFS_KEY_FCM_TOKEN, null) + } + + override fun storeFcmToken(token: String?) { + sharedPrefs.edit { + putString(PREFS_KEY_FCM_TOKEN, token) + } + } + + 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(context)) { + try { + FirebaseMessaging.getInstance().token + .addOnSuccessListener { token -> + storeFcmToken(token) + if (registerPusher) { + pushersManager.enqueueRegisterPusherWithFcmKey(token) + } + } + .addOnFailureListener { e -> + Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") + } + } catch (e: Throwable) { + Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") + } + } else { + Toast.makeText(context, StringR.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show() + Timber.e("No valid Google Play Services found. Cannot use FCM.") + } + } + + /** + * Check the device to make sure it has the Google Play Services APK. If + * it doesn't, display a dialog that allows users to download the APK from + * the Google Play Store or enable it in the device's system settings. + */ + private fun checkPlayServices(context: Context): Boolean { + val apiAvailability = GoogleApiAvailability.getInstance() + val resultCode = apiAvailability.isGooglePlayServicesAvailable(context) + return resultCode == ConnectionResult.SUCCESS + } + + /* + override fun onEnterForeground(activeSessionHolder: ActiveSessionHolder) { + // No op + } + + override fun onEnterBackground(activeSessionHolder: ActiveSessionHolder) { + // No op + } + */ + + companion object { + private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GuardServiceStarter.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GuardServiceStarter.kt new file mode 100644 index 00000000000..42993828a98 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GuardServiceStarter.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +interface GuardServiceStarter { + fun start() {} + fun stop() {} +} + +@ContributesBinding(AppScope::class) +class NoopGuardServiceStarter @Inject constructor() : GuardServiceStarter { + +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/KeepInternalDistributor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/KeepInternalDistributor.kt new file mode 100644 index 00000000000..d351067e524 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/KeepInternalDistributor.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +/** + * UnifiedPush lib tracks an action to check installed and uninstalled distributors. + * We declare it to keep the background sync as an internal unifiedpush distributor. + * This class is used to declare this action. + */ +class KeepInternalDistributor : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) {} +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt new file mode 100644 index 00000000000..2e87f360a54 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl + +import io.element.android.libraries.push.impl.config.PushConfig +import io.element.android.libraries.toolbox.api.appname.AppNameProvider +import java.util.UUID +import javax.inject.Inject + +internal const val DEFAULT_PUSHER_FILE_TAG = "mobile" + +// TODO EAx Communicate with the SDK +class PushersManager @Inject constructor( + private val unifiedPushHelper: UnifiedPushHelper, + // private val activeSessionHolder: ActiveSessionHolder, + // private val localeProvider: LocaleProvider, + private val appNameProvider: AppNameProvider, + // private val getDeviceInfoUseCase: GetDeviceInfoUseCase, +) { + suspend fun testPush() { + /* + val currentSession = activeSessionHolder.getActiveSession() + + currentSession.pushersService().testPush( + unifiedPushHelper.getPushGateway() ?: return, + PushConfig.pusher_app_id, + unifiedPushHelper.getEndpointOrToken().orEmpty(), + TEST_EVENT_ID + ) + + */ + } + + fun enqueueRegisterPusherWithFcmKey(pushKey: String): UUID { + return enqueueRegisterPusher(pushKey, PushConfig.pusher_http_url) + } + + fun enqueueRegisterPusher( + pushKey: String, + gateway: String + ): UUID { + /* + val currentSession = activeSessionHolder.getActiveSession() + val pusher = createHttpPusher(pushKey, gateway) + return currentSession.pushersService().enqueueAddHttpPusher(pusher) + + */ + // TODO EAx + TODO() + } + + private fun createHttpPusher( + pushKey: String, + gateway: String + ): Any = TODO() + /* + HttpPusher( + pushkey = pushKey, + appId = PushConfig.pusher_app_id, + profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(activeSessionHolder.getActiveSession().myUserId.hashCode()), + lang = localeProvider.current().language, + appDisplayName = appNameProvider.getAppName(), + deviceDisplayName = getDeviceInfoUseCase.execute().displayName().orEmpty(), + url = gateway, + enabled = true, + deviceId = activeSessionHolder.getActiveSession().sessionParams.deviceId ?: "MOBILE", + append = false, + withEventIdOnly = true, + ) + + */ + + suspend fun registerEmailForPush(email: String) { + TODO() + /* + val currentSession = activeSessionHolder.getActiveSession() + val appName = appNameProvider.getAppName() + currentSession.pushersService().addEmailPusher( + email = email, + lang = localeProvider.current().language, + emailBranding = appName, + appDisplayName = appName, + deviceDisplayName = currentSession.sessionParams.deviceId ?: "MOBILE" + ) + + */ + } + + fun getPusherForCurrentSession() {}/*: Pusher? { + val session = activeSessionHolder.getSafeActiveSession() ?: return null + val deviceId = session.sessionParams.deviceId + return session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } + } + */ + + + suspend fun unregisterEmailPusher(email: String) { + // val currentSession = activeSessionHolder.getSafeActiveSession() ?: return + // currentSession.pushersService().removeEmailPusher(email) + } + + suspend fun unregisterPusher(pushKey: String) { + // val currentSession = activeSessionHolder.getSafeActiveSession() ?: return + // currentSession.pushersService().removeHttpPusher(pushKey, PushConfig.pusher_app_id) + } + + companion object { + const val TEST_EVENT_ID = "\$THIS_IS_A_FAKE_EVENT_ID" + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/RegisterUnifiedPushUseCase.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/RegisterUnifiedPushUseCase.kt new file mode 100644 index 00000000000..e9f8cb985fc --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/RegisterUnifiedPushUseCase.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl + +import android.content.Context +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.push.impl.config.PushConfig +import org.unifiedpush.android.connector.UnifiedPush +import javax.inject.Inject + +class RegisterUnifiedPushUseCase @Inject constructor( + @ApplicationContext private val context: Context, +) { + + sealed interface RegisterUnifiedPushResult { + object Success : RegisterUnifiedPushResult + object NeedToAskUserForDistributor : RegisterUnifiedPushResult + } + + fun execute(distributor: String = ""): RegisterUnifiedPushResult { + if (distributor.isNotEmpty()) { + saveAndRegisterApp(distributor) + return RegisterUnifiedPushResult.Success + } + + if (!PushConfig.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 + } + } + + private fun saveAndRegisterApp(distributor: String) { + UnifiedPush.saveDistributor(context, distributor) + registerApp() + } + + private fun registerApp() { + UnifiedPush.registerApp(context) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt new file mode 100644 index 00000000000..368f2e23361 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt @@ -0,0 +1,180 @@ +/* + * 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 io.element.android.libraries.push.impl + +import android.content.Context +import io.element.android.libraries.androidutils.system.getApplicationLabel +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.push.impl.config.PushConfig +import io.element.android.libraries.toolbox.api.strings.StringProvider +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.unifiedpush.android.connector.UnifiedPush +import timber.log.Timber +import java.net.URL +import javax.inject.Inject +import io.element.android.libraries.ui.strings.R as StringR + +class UnifiedPushHelper @Inject constructor( + @ApplicationContext private val context: Context, + private val unifiedPushStore: UnifiedPushStore, + // private val matrix: Matrix, + private val fcmHelper: FcmHelper, + private val stringProvider: StringProvider, +) { + + /* TODO EAx + @MainThread + fun showSelectDistributorDialog( + context: Context, + onDistributorSelected: (String) -> Unit, + ) { + val internalDistributorName = stringProvider.getString( + if (fcmHelper.isFirebaseAvailable()) { + StringR.string.unifiedpush_distributor_fcm_fallback + } else { + StringR.string.unifiedpush_distributor_background_sync + } + ) + + val distributors = UnifiedPush.getDistributors(context) + val distributorsName = distributors.map { + if (it == context.packageName) { + internalDistributorName + } else { + context.getApplicationLabel(it) + } + } + + MaterialAlertDialogBuilder(context) + .setTitle(stringProvider.getString(StringR.string.unifiedpush_getdistributors_dialog_title)) + .setItems(distributorsName.toTypedArray()) { _, which -> + val distributor = distributors[which] + onDistributorSelected(distributor) + } + .setOnCancelListener { + // 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() + } + + */ + + @Serializable + internal data class DiscoveryResponse( + @SerialName("unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush() + ) + + @Serializable + internal data class DiscoveryUnifiedPush( + @SerialName("gateway") val gateway: String = "" + ) + + suspend fun storeCustomOrDefaultGateway( + endpoint: String, + onDoneRunnable: Runnable? = null + ) { + // if we use the embedded distributor, + // register app_id type upfcm on sygnal + // the pushkey if FCM key + if (UnifiedPush.getDistributor(context) == context.packageName) { + unifiedPushStore.storePushGateway(PushConfig.pusher_http_url) + onDoneRunnable?.run() + return + } + /* TODO EAx UnifiedPush + // else, unifiedpush, and pushkey is an endpoint + val gateway = PushConfig.default_push_gateway_http_url + val parsed = URL(endpoint) + val custom = "${parsed.protocol}://${parsed.host}/_matrix/push/v1/notify" + Timber.i("Testing $custom") + try { + val response = matrix.rawService().getUrl(custom, CacheStrategy.NoCache) + tryOrNull { Json.decodeFromString(response) } + ?.let { discoveryResponse -> + if (discoveryResponse.unifiedpush.gateway == "matrix") { + Timber.d("Using custom gateway") + unifiedPushStore.storePushGateway(custom) + onDoneRunnable?.run() + return + } + } + } catch (e: Throwable) { + Timber.d(e, "Cannot try custom gateway") + } + unifiedPushStore.storePushGateway(gateway) + onDoneRunnable?.run() + + */ + } + + fun getExternalDistributors(): List { + return UnifiedPush.getDistributors(context) + .filterNot { it == context.packageName } + } + + fun getCurrentDistributorName(): String { + return when { + isEmbeddedDistributor() -> stringProvider.getString(StringR.string.unifiedpush_distributor_fcm_fallback) + isBackgroundSync() -> stringProvider.getString(StringR.string.unifiedpush_distributor_background_sync) + else -> context.getApplicationLabel(UnifiedPush.getDistributor(context)) + } + } + + fun isEmbeddedDistributor(): Boolean { + return isInternalDistributor() && fcmHelper.isFirebaseAvailable() + } + + fun isBackgroundSync(): Boolean { + return isInternalDistributor() && !fcmHelper.isFirebaseAvailable() + } + + private fun isInternalDistributor(): Boolean { + return UnifiedPush.getDistributor(context).isEmpty() || + UnifiedPush.getDistributor(context) == context.packageName + } + + fun getPrivacyFriendlyUpEndpoint(): String? { + val endpoint = getEndpointOrToken() + if (endpoint.isNullOrEmpty()) return null + if (isEmbeddedDistributor()) { + return endpoint + } + return try { + val parsed = URL(endpoint) + "${parsed.protocol}://${parsed.host}/***" + } catch (e: Exception) { + Timber.e(e, "Error parsing unifiedpush endpoint") + null + } + } + + fun getEndpointOrToken(): String? { + return if (isEmbeddedDistributor()) fcmHelper.getFcmToken() + else unifiedPushStore.getEndpoint() + } + + fun getPushGateway(): String? { + return if (isEmbeddedDistributor()) PushConfig.pusher_http_url + else unifiedPushStore.getPushGateway() + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.kt new file mode 100644 index 00000000000..5e2e33d7d3e --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.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 io.element.android.libraries.push.impl + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.DefaultPreferences +import javax.inject.Inject + +/** + * TODO EAx Store in BDD (for multisession) + */ +class UnifiedPushStore @Inject constructor( + @ApplicationContext val context: Context, + @DefaultPreferences private val defaultPrefs: SharedPreferences, +) { + /** + * Retrieves the UnifiedPush Endpoint. + * + * @return the UnifiedPush Endpoint or null if not received + */ + fun getEndpoint(): String? { + return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN, null) + } + + /** + * Store UnifiedPush Endpoint to the SharedPrefs. + * + * @param endpoint the endpoint to store + */ + fun storeUpEndpoint(endpoint: String?) { + defaultPrefs.edit { + putString(PREFS_ENDPOINT_OR_TOKEN, endpoint) + } + } + + /** + * Retrieves the Push Gateway. + * + * @return the Push Gateway or null if not defined + */ + fun getPushGateway(): String? { + return defaultPrefs.getString(PREFS_PUSH_GATEWAY, null) + } + + /** + * Store Push Gateway to the SharedPrefs. + * + * @param gateway the push gateway to store + */ + fun storePushGateway(gateway: String?) { + defaultPrefs.edit { + putString(PREFS_PUSH_GATEWAY, gateway) + } + } + + companion object { + private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN" + private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY" + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnregisterUnifiedPushUseCase.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnregisterUnifiedPushUseCase.kt new file mode 100644 index 00000000000..34c78a237e2 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnregisterUnifiedPushUseCase.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 io.element.android.libraries.push.impl + +import android.content.Context +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.push.api.model.BackgroundSyncMode +import io.element.android.libraries.push.api.store.PushDataStore +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 pushDataStore: PushDataStore, + private val unifiedPushStore: UnifiedPushStore, + private val unifiedPushHelper: UnifiedPushHelper, +) { + + suspend fun execute(pushersManager: PushersManager?) { + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME + pushDataStore.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/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt new file mode 100644 index 00000000000..09dbf28a332 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl + +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.push.api.store.PushDataStore +import io.element.android.libraries.push.impl.config.PushConfig +import io.element.android.libraries.push.impl.di.FirebaseMessagingServiceBindings +import io.element.android.libraries.push.impl.parser.PushParser +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("Push", LoggerTag.SYNC) + +class VectorFirebaseMessagingService : FirebaseMessagingService() { + @Inject lateinit var fcmHelper: FcmHelper + @Inject lateinit var pushDataStore: PushDataStore + // @Inject lateinit var activeSessionHolder: ActiveSessionHolder + @Inject lateinit var pushersManager: PushersManager + @Inject lateinit var pushParser: PushParser + @Inject lateinit var vectorPushHandler: VectorPushHandler + @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + + override fun onCreate() { + super.onCreate() + applicationContext.bindings().inject(this) + } + + override fun onNewToken(token: String) { + Timber.tag(loggerTag.value).d("New Firebase token") + fcmHelper.storeFcmToken(token) + if ( + pushDataStore.areNotificationEnabledForDevice() && + // TODO EAx activeSessionHolder.hasActiveSession() && + unifiedPushHelper.isEmbeddedDistributor() + ) { + pushersManager.enqueueRegisterPusher(token, PushConfig.pusher_http_url) + } + } + + override fun onMessageReceived(message: RemoteMessage) { + Timber.tag(loggerTag.value).d("New Firebase message") + pushParser.parsePushDataFcm(message.data).let { + vectorPushHandler.handle(it) + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt new file mode 100644 index 00000000000..9fad5a37bce --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt @@ -0,0 +1,188 @@ +/* + * 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 io.element.android.libraries.push.impl + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import io.element.android.libraries.androidutils.network.WifiDetector +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.push.api.store.PushDataStore +import io.element.android.libraries.push.impl.model.PushData +import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver +import io.element.android.libraries.push.impl.notifications.NotificationActionIds +import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager +import io.element.android.libraries.push.impl.store.DefaultPushDataStore +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("Push", LoggerTag.SYNC) + +class VectorPushHandler @Inject constructor( + private val notificationDrawerManager: NotificationDrawerManager, + private val notifiableEventResolver: NotifiableEventResolver, + // private val activeSessionHolder: ActiveSessionHolder, + private val pushDataStore: PushDataStore, + private val defaultPushDataStore: DefaultPushDataStore, + private val actionIds: NotificationActionIds, + @ApplicationContext private val context: Context, + private val buildMeta: BuildMeta +) { + + private val coroutineScope = CoroutineScope(SupervisorJob()) + private val wifiDetector: WifiDetector = WifiDetector(context) + + // UI handler + private val mUIHandler by lazy { + Handler(Looper.getMainLooper()) + } + + /** + * Called when message is received. + * + * @param pushData the data received in the push. + */ + fun handle(pushData: PushData) { + Timber.tag(loggerTag.value).d("## handling pushData") + + if (buildMeta.lowPrivacyLoggingEnabled) { + Timber.tag(loggerTag.value).d("## pushData: $pushData") + } + + runBlocking { + defaultPushDataStore.incrementPushCounter() + } + + // Diagnostic Push + if (pushData.eventId == PushersManager.TEST_EVENT_ID) { + val intent = Intent(actionIds.push) + LocalBroadcastManager.getInstance(context).sendBroadcast(intent) + return + } + + if (!pushDataStore.areNotificationEnabledForDevice()) { + Timber.tag(loggerTag.value).i("Notification are disabled for this device") + return + } + + mUIHandler.post { + if (ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { + // we are in foreground, let the sync do the things? + Timber.tag(loggerTag.value).d("PUSH received in a foreground state, ignore") + } else { + coroutineScope.launch(Dispatchers.IO) { handleInternal(pushData) } + } + } + } + + /** + * Internal receive method. + * + * @param pushData Object containing message data. + */ + private suspend fun handleInternal(pushData: PushData) { + try { + if (buildMeta.lowPrivacyLoggingEnabled) { + Timber.tag(loggerTag.value).d("## handleInternal() : $pushData") + } else { + Timber.tag(loggerTag.value).d("## handleInternal()") + } + + /* TODO EAx + val session = activeSessionHolder.getOrInitializeSession() + + if (session == null) { + Timber.tag(loggerTag.value).w("## Can't sync from push, no current session") + } else { + if (isEventAlreadyKnown(pushData)) { + Timber.tag(loggerTag.value).d("Ignoring push, event already known") + } else { + // Try to get the Event content faster + Timber.tag(loggerTag.value).d("Requesting event in fast lane") + getEventFastLane(session, pushData) + + Timber.tag(loggerTag.value).d("Requesting background sync") + session.syncService().requireBackgroundSync() + } + } + + */ + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## handleInternal() failed") + } + } + + /* TODO EAx + private suspend fun getEventFastLane(session: Session, pushData: PushData) { + pushData.roomId ?: return + pushData.eventId ?: return + + if (wifiDetector.isConnectedToWifi().not()) { + Timber.tag(loggerTag.value).d("No WiFi network, do not get Event") + return + } + + Timber.tag(loggerTag.value).d("Fast lane: start request") + val event = tryOrNull { session.eventService().getEvent(pushData.roomId, pushData.eventId) } ?: return + + val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event, canBeReplaced = true) + + if (resolvedEvent is NotifiableMessageEvent) { + // If the room is currently displayed, we will not show a notification, so no need to get the Event faster + if (notificationDrawerManager.shouldIgnoreMessageEventInRoom(resolvedEvent)) { + return + } + } + + resolvedEvent + ?.also { Timber.tag(loggerTag.value).d("Fast lane: notify drawer") } + ?.let { + notificationDrawerManager.updateEvents { it.onNotifiableEventReceived(resolvedEvent) } + } + } + + */ + + // check if the event was not yet received + // a previous catchup might have already retrieved the notified event + private fun isEventAlreadyKnown(pushData: PushData): Boolean { + /* TODO EAx + if (pushData.eventId != null && pushData.roomId != null) { + try { + val session = activeSessionHolder.getSafeActiveSession() ?: return false + val room = session.getRoom(pushData.roomId) ?: return false + return room.getTimelineEvent(pushData.eventId) != null + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined") + } + } + + */ + return false + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorUnifiedPushMessagingReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorUnifiedPushMessagingReceiver.kt new file mode 100644 index 00000000000..0fcdbafb698 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorUnifiedPushMessagingReceiver.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 io.element.android.libraries.push.impl + +import android.content.Context +import android.content.Intent +import android.widget.Toast +import io.element.android.libraries.architecture.bindings + +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.push.api.model.BackgroundSyncMode +import io.element.android.libraries.push.api.store.PushDataStore +import io.element.android.libraries.push.impl.di.VectorUnifiedPushMessagingReceiverBindings +import io.element.android.libraries.push.impl.parser.PushParser +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.unifiedpush.android.connector.MessagingReceiver +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("Push", LoggerTag.SYNC) + +class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { + @Inject lateinit var pushersManager: PushersManager + @Inject lateinit var pushParser: PushParser + + //@Inject lateinit var activeSessionHolder: ActiveSessionHolder + @Inject lateinit var pushDataStore: PushDataStore + @Inject lateinit var vectorPushHandler: VectorPushHandler + @Inject lateinit var guardServiceStarter: GuardServiceStarter + @Inject lateinit var unifiedPushStore: UnifiedPushStore + @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + + private val coroutineScope = CoroutineScope(SupervisorJob()) + + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + // Inject + context.applicationContext.bindings().inject(this) + } + + /** + * Called when message is received. + * + * @param context the Android context + * @param message the message + * @param instance connection, for multi-account + */ + override fun onMessage(context: Context, message: ByteArray, instance: String) { + Timber.tag(loggerTag.value).d("New message") + pushParser.parsePushDataUnifiedPush(message)?.let { + vectorPushHandler.handle(it) + } ?: run { + Timber.tag(loggerTag.value).w("Invalid received data Json format") + } + } + + override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { + Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint") + if (pushDataStore.areNotificationEnabledForDevice() /* TODO EAx && activeSessionHolder.hasActiveSession() */) { + // If the endpoint has changed + // or the gateway has changed + if (unifiedPushHelper.getEndpointOrToken() != endpoint) { + unifiedPushStore.storeUpEndpoint(endpoint) + coroutineScope.launch { + unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) { + unifiedPushHelper.getPushGateway()?.let { + pushersManager.enqueueRegisterPusher(endpoint, it) + } + } + } + } else { + Timber.tag(loggerTag.value).i("onNewEndpoint: skipped") + } + } + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED + pushDataStore.setFdroidSyncBackgroundMode(mode) + guardServiceStarter.stop() + } + + override fun onRegistrationFailed(context: Context, instance: String) { + Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show() + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME + pushDataStore.setFdroidSyncBackgroundMode(mode) + guardServiceStarter.start() + } + + override fun onUnregistered(context: Context, instance: String) { + Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered") + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME + pushDataStore.setFdroidSyncBackgroundMode(mode) + guardServiceStarter.start() + runBlocking { + try { + pushersManager.unregisterPusher(unifiedPushHelper.getEndpointOrToken().orEmpty()) + } catch (e: Exception) { + Timber.tag(loggerTag.value).d("Probably unregistering a non existing pusher") + } + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/config/PushConfig.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/config/PushConfig.kt new file mode 100644 index 00000000000..d2d1c96506f --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/config/PushConfig.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.config + +object PushConfig { + /** + * It is the push gateway for FCM embedded distributor. + * Note: pusher_http_url should have path '/_matrix/push/v1/notify' --> + */ + const val pusher_http_url: String = "https://matrix.org/_matrix/push/v1/notify" + + /** + * It is the push gateway for UnifiedPush. + * Note: default_push_gateway_http_url should have path '/_matrix/push/v1/notify' + */ + const val default_push_gateway_http_url: String = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify" + + /** + * Note: pusher_app_id cannot exceed 64 chars. + */ + const val pusher_app_id: String = "im.vector.app.android" + + /** + * Set to true to allow external push distributor such as Ntfy. + */ + const val allowExternalUnifiedPushDistributors: Boolean = false +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/FirebaseMessagingServiceBindings.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/FirebaseMessagingServiceBindings.kt new file mode 100644 index 00000000000..1de015b7706 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/FirebaseMessagingServiceBindings.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.impl.VectorFirebaseMessagingService + +@ContributesTo(AppScope::class) +interface FirebaseMessagingServiceBindings { + fun inject(service: VectorFirebaseMessagingService) +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/VectorUnifiedPushMessagingReceiverBindings.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/VectorUnifiedPushMessagingReceiverBindings.kt new file mode 100644 index 00000000000..1a70d94ee4c --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/VectorUnifiedPushMessagingReceiverBindings.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.impl.VectorUnifiedPushMessagingReceiver + +@ContributesTo(AppScope::class) +interface VectorUnifiedPushMessagingReceiverBindings { + fun inject(receiver: VectorUnifiedPushMessagingReceiver) +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt new file mode 100644 index 00000000000..9a91f1f1ac4 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.model + +/** + * Represent parsed data that the app has received from a Push content. + * + * @property eventId The Event ID. If not null, it will not be empty, and will have a valid format. + * @property roomId The Room ID. If not null, it will not be empty, and will have a valid format. + * @property unread Number of unread message. + */ +data class PushData( + val eventId: String?, + val roomId: String?, + val unread: Int?, +) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataFcm.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataFcm.kt new file mode 100644 index 00000000000..0e37c14e125 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataFcm.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 io.element.android.libraries.push.impl.model + +import io.element.android.libraries.matrix.api.core.MatrixPatterns + +/** + * In this case, the format is: + *
+ * {
+ *     "event_id":"$anEventId",
+ *     "room_id":"!aRoomId",
+ *     "unread":"1",
+ *     "prio":"high"
+ * }
+ * 
+ * . + */ +data class PushDataFcm( + val eventId: String?, + val roomId: String?, + var unread: Int?, +) + +fun PushDataFcm.toPushData() = PushData( + eventId = eventId?.takeIf { MatrixPatterns.isEventId(it) }, + roomId = roomId?.takeIf { MatrixPatterns.isRoomId(it) }, + unread = unread +) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataUnifiedPush.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataUnifiedPush.kt new file mode 100644 index 00000000000..c4227b3db2f --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataUnifiedPush.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.model + +import io.element.android.libraries.matrix.api.core.MatrixPatterns +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * In this case, the format is: + *
+ * {
+ *     "notification":{
+ *         "event_id":"$anEventId",
+ *         "room_id":"!aRoomId",
+ *         "counts":{
+ *             "unread":1
+ *         },
+ *         "prio":"high"
+ *     }
+ * }
+ * 
+ * . + */ +@Serializable +data class PushDataUnifiedPush( + val notification: PushDataUnifiedPushNotification? +) + +@Serializable +data class PushDataUnifiedPushNotification( + @SerialName("event_id") val eventId: String?, + @SerialName("room_id") val roomId: String?, + @SerialName("counts") var counts: PushDataUnifiedPushCounts?, +) + +@Serializable +data class PushDataUnifiedPushCounts( + @SerialName("unread") val unread: Int? +) + +fun PushDataUnifiedPush.toPushData() = PushData( + eventId = notification?.eventId?.takeIf { MatrixPatterns.isEventId(it) }, + roomId = notification?.roomId?.takeIf { MatrixPatterns.isRoomId(it) }, + unread = notification?.counts?.unread +) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FilteredEventDetector.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FilteredEventDetector.kt new file mode 100644 index 00000000000..6e92c2ec602 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FilteredEventDetector.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2023 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 io.element.android.libraries.push.impl.notifications + +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import javax.inject.Inject + +class FilteredEventDetector @Inject constructor( + //private val activeSessionDataSource: ActiveSessionDataSource +) { + + /** + * Returns true if the given event should be ignored. + * Used to skip notifications if a non expected message is received. + */ + fun shouldBeIgnored(notifiableEvent: NotifiableEvent): Boolean { + /* TODO EAx + val session = activeSessionDataSource.currentValue?.orNull() ?: return false + + if (notifiableEvent is NotifiableMessageEvent) { + val room = session.getRoom(notifiableEvent.roomId) ?: return false + val timelineEvent = room.getTimelineEvent(notifiableEvent.eventId) ?: return false + return timelineEvent.shouldBeIgnored() + } + + */ + return false + } + + /** + * Whether the timeline event should be ignored. + */ + /* + private fun TimelineEvent.shouldBeIgnored(): Boolean { + if (root.isVoiceMessage()) { + val audioEvent = root.asMessageAudioEvent() + // if the event is a voice message related to a voice broadcast, only show the event on the first chunk. + return audioEvent.isVoiceBroadcast() && audioEvent?.sequence != 1 + } + + return false + } + */ +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt new file mode 100644 index 00000000000..91b62eba0e0 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import io.element.android.libraries.push.impl.AutoAcceptInvites +import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreMessageEventInRoom +import timber.log.Timber +import javax.inject.Inject + +private typealias ProcessedEvents = List> + +class NotifiableEventProcessor @Inject constructor( + private val outdatedDetector: OutdatedEventDetector, + private val autoAcceptInvites: AutoAcceptInvites +) { + + fun process(queuedEvents: List, currentRoomId: String?, currentThreadId: String?, renderedEvents: ProcessedEvents): ProcessedEvents { + val processedEvents = queuedEvents.map { + val type = when (it) { + is InviteNotifiableEvent -> if (autoAcceptInvites.hideInvites) ProcessedEvent.Type.REMOVE else ProcessedEvent.Type.KEEP + is NotifiableMessageEvent -> when { + it.shouldIgnoreMessageEventInRoom(currentRoomId, currentThreadId) -> { + ProcessedEvent.Type.REMOVE + .also { Timber.d("notification message removed due to currently viewing the same room or thread") } + } + outdatedDetector.isMessageOutdated(it) -> ProcessedEvent.Type.REMOVE + .also { Timber.d("notification message removed due to being read") } + else -> ProcessedEvent.Type.KEEP + } + is SimpleNotifiableEvent -> when (it.type) { + /*EventType.REDACTION*/ "m.room.redaction" -> ProcessedEvent.Type.REMOVE + else -> ProcessedEvent.Type.KEEP + } + } + ProcessedEvent(type, it) + } + + val removedEventsDiff = renderedEvents.filter { renderedEvent -> + queuedEvents.none { it.eventId == renderedEvent.event.eventId } + }.map { ProcessedEvent(ProcessedEvent.Type.REMOVE, it.event) } + + return removedEventsDiff + processedEvents + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt new file mode 100644 index 00000000000..778fe20d7b1 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -0,0 +1,264 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.element.android.libraries.push.impl.notifications + +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.toolbox.api.strings.StringProvider +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.toolbox.api.systemclock.SystemClock +import javax.inject.Inject + +/** + * The notifiable event resolver is able to create a NotifiableEvent (view model for notifications) from an sdk Event. + * It is used as a bridge between the Event Thread and the NotificationDrawerManager. + * The NotifiableEventResolver is the only aware of session/store, the NotificationDrawerManager has no knowledge of that, + * this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk. + */ +class NotifiableEventResolver @Inject constructor( + private val stringProvider: StringProvider, + // private val noticeEventFormatter: NoticeEventFormatter, + // private val displayableEventFormatter: DisplayableEventFormatter, + private val clock: SystemClock, + private val buildMeta: BuildMeta, +) { + + suspend fun resolveEvent(/*event: Event, session: Session, isNoisy: Boolean*/): NotifiableEvent? { + return TODO() + /* + val roomID = event.roomId ?: return null + val eventId = event.eventId ?: return null + if (event.getClearType() == EventType.STATE_ROOM_MEMBER) { + return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy) + } + val timelineEvent = session.getRoom(roomID)?.getTimelineEvent(eventId) ?: return null + return when { + event.supportsNotification() || event.type == EventType.ENCRYPTED -> { + resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy) + } + else -> { + // If the event can be displayed, display it as is + Timber.w("NotifiableEventResolver Received an unsupported event matching a bing rule") + // TODO Better event text display + val bodyPreview = event.type ?: EventType.MISSING_TYPE + + SimpleNotifiableEvent( + session.myUserId, + eventId = event.eventId!!, + editedEventId = timelineEvent.getEditedEventId(), + noisy = false, // will be updated + timestamp = event.originServerTs ?: clock.epochMillis(), + description = bodyPreview, + title = stringProvider.getString(StringR.string.notification_unknown_new_event), + soundName = null, + type = event.type, + canBeReplaced = false + ) + } + } + + */ + } + + suspend fun resolveInMemoryEvent(/*session: Session, event: Event, canBeReplaced: Boolean*/): NotifiableEvent? { + TODO() + /* + if (!event.supportsNotification()) return null + + // Ignore message edition + if (event.isEdition()) return null + + val actions = session.pushRuleService().getActions(event) + val notificationAction = actions.toNotificationAction() + + return if (notificationAction.shouldNotify) { + val user = session.getUserOrDefault(event.senderId!!) + + val timelineEvent = TimelineEvent( + root = event, + localId = -1, + eventId = event.eventId!!, + displayIndex = 0, + senderInfo = SenderInfo( + userId = user.userId, + displayName = user.toMatrixItem().getBestName(), + isUniqueDisplayName = true, + avatarUrl = user.avatarUrl + ) + ) + resolveMessageEvent(timelineEvent, session, canBeReplaced = canBeReplaced, isNoisy = !notificationAction.soundName.isNullOrBlank()) + } else { + Timber.d("Matched push rule is set to not notify") + null + } + + */ + } + + private suspend fun resolveMessageEvent(/*event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean*/): NotifiableMessageEvent? { + TODO() + /* + // The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) + val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/) + + return if (room == null) { + Timber.e("## Unable to resolve room for eventId [$event]") + // Ok room is not known in store, but we can still display something + val body = displayableEventFormatter.format(event, isDm = false, appendAuthor = false) + val roomName = stringProvider.getString(StringR.string.notification_unknown_room_name) + val senderDisplayName = event.senderInfo.disambiguatedDisplayName + + NotifiableMessageEvent( + eventId = event.root.eventId!!, + editedEventId = event.getEditedEventId(), + canBeReplaced = canBeReplaced, + timestamp = event.root.originServerTs ?: 0, + noisy = isNoisy, + senderName = senderDisplayName, + senderId = event.root.senderId, + body = body.toString(), + imageUriString = event.fetchImageIfPresent(session)?.toString(), + roomId = event.root.roomId!!, + threadId = event.root.getRootThreadEventId(), + roomName = roomName, + matrixID = session.myUserId + ) + } else { + event.attemptToDecryptIfNeeded(session) + // only convert encrypted messages to NotifiableMessageEvents + when { + event.root.supportsNotification() -> { + val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString() + val roomName = room.roomSummary()?.displayName ?: "" + val senderDisplayName = event.senderInfo.disambiguatedDisplayName + + NotifiableMessageEvent( + eventId = event.root.eventId!!, + editedEventId = event.getEditedEventId(), + canBeReplaced = canBeReplaced, + timestamp = event.root.originServerTs ?: 0, + noisy = isNoisy, + senderName = senderDisplayName, + senderId = event.root.senderId, + body = body, + imageUriString = event.fetchImageIfPresent(session)?.toString(), + roomId = event.root.roomId!!, + threadId = event.root.getRootThreadEventId(), + roomName = roomName, + roomIsDirect = room.roomSummary()?.isDirect ?: false, + roomAvatarPath = session.contentUrlResolver() + .resolveThumbnail( + room.roomSummary()?.avatarUrl, + 250, + 250, + ContentUrlResolver.ThumbnailMethod.SCALE + ), + senderAvatarPath = session.contentUrlResolver() + .resolveThumbnail( + event.senderInfo.avatarUrl, + 250, + 250, + ContentUrlResolver.ThumbnailMethod.SCALE + ), + matrixID = session.myUserId, + soundName = null + ) + } + else -> null + } + } + + */ + } + + /* + private suspend fun TimelineEvent.attemptToDecryptIfNeeded(session: Session) { + if (root.isEncrypted() && root.mxDecryptionResult == null) { + // TODO use a global event decryptor? attache to session and that listen to new sessionId? + // for now decrypt sync + try { + val result = session.cryptoService().decryptEvent(root, root.roomId + UUID.randomUUID().toString()) + root.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe + ) + } catch (ignore: MXCryptoError) { + } + } + } + */ + + /* + private suspend fun TimelineEvent.fetchImageIfPresent(session: Session): Uri? { + return when { + root.isEncrypted() && root.mxDecryptionResult == null -> null + root.isImageMessage() -> downloadAndExportImage(session) + else -> null + } + } + */ + + /* + private suspend fun TimelineEvent.downloadAndExportImage(session: Session): Uri? { + return kotlin.runCatching { + getVectorLastMessageContent()?.takeAs()?.let { imageMessage -> + val fileService = session.fileService() + fileService.downloadFile(imageMessage) + fileService.getTemporarySharableURI(imageMessage) + } + }.onFailure { + Timber.e(it, "Failed to download and export image for notification") + }.getOrNull() + } + */ + + /* + private fun resolveStateRoomEvent(event: Event, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent? { + val content = event.content?.toModel() ?: return null + val roomId = event.roomId ?: return null + val dName = event.senderId?.let { session.roomService().getRoomMember(it, roomId)?.displayName } + if (Membership.INVITE == content.membership) { + val roomSummary = session.getRoomSummary(roomId) + val body = noticeEventFormatter.format(event, dName, isDm = roomSummary?.isDirect.orFalse()) + ?: stringProvider.getString(StringR.string.notification_new_invitation) + return InviteNotifiableEvent( + session.myUserId, + eventId = event.eventId!!, + editedEventId = null, + canBeReplaced = canBeReplaced, + roomId = roomId, + roomName = roomSummary?.displayName, + timestamp = event.originServerTs ?: 0, + noisy = isNoisy, + title = stringProvider.getString(StringR.string.notification_new_invitation), + description = body.toString(), + soundName = null, // will be set later + type = event.getClearType() + ) + } else { + Timber.e("## unsupported notifiable event for eventId [${event.eventId}]") + if (buildMeta.lowPrivacyLoggingEnabled) { + Timber.e("## unsupported notifiable event for event [$event]") + } + // TODO generic handling? + } + return null + } + */ +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationAction.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationAction.kt new file mode 100644 index 00000000000..31ec28d0230 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationAction.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.element.android.libraries.push.impl.notifications + +data class NotificationAction( + val shouldNotify: Boolean, + val highlight: Boolean, + val soundName: String? +) + +/* +fun List.toNotificationAction(): NotificationAction { + var shouldNotify = false + var highlight = false + var sound: String? = null + forEach { action -> + when (action) { + is Action.Notify -> shouldNotify = true + is Action.DoNotNotify -> shouldNotify = false + is Action.Highlight -> highlight = action.highlight + is Action.Sound -> sound = action.sound + } + } + return NotificationAction(shouldNotify, highlight, sound) +} + */ diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt new file mode 100644 index 00000000000..b2a71299987 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import io.element.android.libraries.core.meta.BuildMeta +import javax.inject.Inject + +/** + * Util class for creating notifications. + * Note: Cannot inject ColorProvider in the constructor, because it requires an Activity + */ + +data class NotificationActionIds @Inject constructor( + private val buildMeta: BuildMeta, +) { + + val join = "${buildMeta.applicationId}.NotificationActions.JOIN_ACTION" + val reject = "${buildMeta.applicationId}.NotificationActions.REJECT_ACTION" + val quickLaunch = "${buildMeta.applicationId}.NotificationActions.QUICK_LAUNCH_ACTION" + val markRoomRead = "${buildMeta.applicationId}.NotificationActions.MARK_ROOM_READ_ACTION" + val smartReply = "${buildMeta.applicationId}.NotificationActions.SMART_REPLY_ACTION" + val dismissSummary = "${buildMeta.applicationId}.NotificationActions.DISMISS_SUMMARY_ACTION" + val dismissRoom = "${buildMeta.applicationId}.NotificationActions.DISMISS_ROOM_NOTIF_ACTION" + val tapToView = "${buildMeta.applicationId}.NotificationActions.TAP_TO_VIEW_ACTION" + val diagnostic = "${buildMeta.applicationId}.NotificationActions.DIAGNOSTIC" + val push = "${buildMeta.applicationId}.PUSH" +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt new file mode 100644 index 00000000000..d0ab0237894 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.content.Context +import android.graphics.Bitmap +import android.os.Build +import androidx.annotation.WorkerThread +import androidx.core.graphics.drawable.IconCompat +import io.element.android.libraries.di.ApplicationContext +import timber.log.Timber +import javax.inject.Inject + +class NotificationBitmapLoader @Inject constructor( + @ApplicationContext private val context: Context +) { + + /** + * Get icon of a room. + */ + @WorkerThread + fun getRoomBitmap(path: String?): Bitmap? { + if (path == null) { + return null + } + return loadRoomBitmap(path) + } + + @WorkerThread + private fun loadRoomBitmap(path: String): Bitmap? { + return try { + null + /* TODO Notification + Glide.with(context) + .asBitmap() + .load(path) + .format(DecodeFormat.PREFER_ARGB_8888) + .signature(ObjectKey("room-icon-notification")) + .submit() + .get() + */ + } catch (e: Exception) { + Timber.e(e, "decodeFile failed") + null + } + } + + /** + * Get icon of a user. + * Before Android P, this does nothing because the icon won't be used + */ + @WorkerThread + fun getUserIcon(path: String?): IconCompat? { + if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + return null + } + + return loadUserIcon(path) + } + + @WorkerThread + private fun loadUserIcon(path: String): IconCompat? { + return try { + null + /* TODO Notification + val bitmap = Glide.with(context) + .asBitmap() + .load(path) + .transform(CircleCrop()) + .format(DecodeFormat.PREFER_ARGB_8888) + .signature(ObjectKey("user-icon-notification")) + .submit() + .get() + IconCompat.createWithBitmap(bitmap) + */ + } catch (e: Exception) { + Timber.e(e, "decodeFile failed") + null + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt new file mode 100644 index 00000000000..ea46654d88b --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt @@ -0,0 +1,247 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.core.app.RemoteInput +import io.element.android.libraries.analytics.api.AnalyticsTracker +import io.element.android.libraries.analytics.api.plan.JoinedRoom +import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.toolbox.api.systemclock.SystemClock +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.UUID +import javax.inject.Inject + +import io.element.android.libraries.ui.strings.R as StringR + +/** + * Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.). + */ +class NotificationBroadcastReceiver : BroadcastReceiver() { + + @Inject lateinit var notificationDrawerManager: NotificationDrawerManager + //@Inject lateinit var activeSessionHolder: ActiveSessionHolder + @Inject lateinit var analyticsTracker: AnalyticsTracker + @Inject lateinit var clock: SystemClock + @Inject lateinit var actionIds: NotificationActionIds + + override fun onReceive(context: Context?, intent: Intent?) { + if (intent == null || context == null) return + context.bindings().inject(this) + Timber.v("NotificationBroadcastReceiver received : $intent") + when (intent.action) { + actionIds.smartReply -> + handleSmartReply(intent, context) + actionIds.dismissRoom -> + intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> + notificationDrawerManager.updateEvents { it.clearMessagesForRoom(roomId) } + } + actionIds.dismissSummary -> + notificationDrawerManager.clearAllEvents() + actionIds.markRoomRead -> + intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> + notificationDrawerManager.updateEvents { it.clearMessagesForRoom(roomId) } + handleMarkAsRead(roomId) + } + actionIds.join -> { + intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> + notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomId) } + handleJoinRoom(roomId) + } + } + actionIds.reject -> { + intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> + notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomId) } + handleRejectRoom(roomId) + } + } + } + } + + private fun handleJoinRoom(roomId: String) { + /* + activeSessionHolder.getSafeActiveSession()?.let { session -> + val room = session.getRoom(roomId) + if (room != null) { + session.coroutineScope.launch { + tryOrNull { + session.roomService().joinRoom(room.roomId) + analyticsTracker.capture(room.roomSummary().toAnalyticsJoinedRoom(JoinedRoom.Trigger.Notification)) + } + } + } + } + + */ + } + + private fun handleRejectRoom(roomId: String) { + /* + activeSessionHolder.getSafeActiveSession()?.let { session -> + session.coroutineScope.launch { + tryOrNull { session.roomService().leaveRoom(roomId) } + } + } + + */ + } + + private fun handleMarkAsRead(roomId: String) { + /* + activeSessionHolder.getActiveSession().let { session -> + val room = session.getRoom(roomId) + if (room != null) { + session.coroutineScope.launch { + tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, mainTimeLineOnly = false) } + } + } + } + + */ + } + + private fun handleSmartReply(intent: Intent, context: Context) { + val message = getReplyMessage(intent) + val roomId = intent.getStringExtra(KEY_ROOM_ID) + val threadId = intent.getStringExtra(KEY_THREAD_ID) + + if (message.isNullOrBlank() || roomId.isNullOrBlank()) { + // ignore this event + // Can this happen? should we update notification? + return + } + /* + activeSessionHolder.getActiveSession().let { session -> + session.getRoom(roomId)?.let { room -> + sendMatrixEvent(message, threadId, session, room, context) + } + } + + */ + } + + /* + private fun sendMatrixEvent(message: String, threadId: String?, session: Session, room: Room, context: Context?) { + if (threadId != null) { + room.relationService().replyInThread( + rootThreadEventId = threadId, + replyInThreadText = message, + ) + } else { + room.sendService().sendTextMessage(message) + } + + // Create a new event to be displayed in the notification drawer, right now + + val notifiableMessageEvent = NotifiableMessageEvent( + // Generate a Fake event id + eventId = UUID.randomUUID().toString(), + editedEventId = null, + noisy = false, + timestamp = clock.epochMillis(), + senderName = session.roomService().getRoomMember(session.myUserId, room.roomId)?.displayName + ?: context?.getString(StringR.string.notification_sender_me), + senderId = session.myUserId, + body = message, + imageUriString = null, + roomId = room.roomId, + threadId = threadId, + roomName = room.roomSummary()?.displayName ?: room.roomId, + roomIsDirect = room.roomSummary()?.isDirect == true, + outGoingMessage = true, + canBeReplaced = false + ) + + notificationDrawerManager.updateEvents { it.onNotifiableEventReceived(notifiableMessageEvent) } + + /* + // TODO Error cannot be managed the same way than in Riot + + val event = Event(mxMessage, session.credentials.userId, roomId) + room.storeOutgoingEvent(event) + room.sendEvent(event, object : MatrixCallback { + override fun onSuccess(info: Void?) { + Timber.v("Send message : onSuccess ") + } + + override fun onNetworkError(e: Exception) { + Timber.e(e, "Send message : onNetworkError") + onSmartReplyFailed(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + Timber.v("Send message : onMatrixError " + e.message) + if (e is MXCryptoError) { + Toast.makeText(context, e.detailedErrorDescription, Toast.LENGTH_SHORT).show() + onSmartReplyFailed(e.detailedErrorDescription) + } else { + Toast.makeText(context, e.localizedMessage, Toast.LENGTH_SHORT).show() + onSmartReplyFailed(e.localizedMessage) + } + } + + override fun onUnexpectedError(e: Exception) { + Timber.e(e, "Send message : onUnexpectedError " + e.message) + onSmartReplyFailed(e.message) + } + + + fun onSmartReplyFailed(reason: String?) { + val notifiableMessageEvent = NotifiableMessageEvent( + event.eventId, + false, + clock.epochMillis(), + session.myUser?.displayname + ?: context?.getString(StringR.string.notification_sender_me), + session.myUserId, + message, + roomId, + room.getRoomDisplayName(context), + room.isDirect) + notifiableMessageEvent.outGoingMessage = true + notifiableMessageEvent.outGoingMessageFailed = true + + VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent) + VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(null) + } + }) + */ + } + + */ + + private fun getReplyMessage(intent: Intent?): String? { + if (intent != null) { + val remoteInput = RemoteInput.getResultsFromIntent(intent) + if (remoteInput != null) { + return remoteInput.getCharSequence(KEY_TEXT_REPLY)?.toString() + } + } + return null + } + + companion object { + const val KEY_ROOM_ID = "roomID" + const val KEY_THREAD_ID = "threadID" + const val KEY_TEXT_REPLY = "key_text_reply" + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverBindings.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverBindings.kt new file mode 100644 index 00000000000..ae936e693bb --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverBindings.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.notifications + +import com.squareup.anvil.annotations.ContributesTo +import io.element.android.libraries.di.AppScope + +@ContributesTo(AppScope::class) +interface NotificationBroadcastReceiverBindings { + fun inject(receiver: NotificationBroadcastReceiver) +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt new file mode 100644 index 00000000000..7f9ec73343b --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.app.Notification +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import io.element.android.libraries.di.ApplicationContext +import timber.log.Timber +import javax.inject.Inject + +class NotificationDisplayer @Inject constructor( + @ApplicationContext context: Context, +) { + + private val notificationManager = NotificationManagerCompat.from(context) + + fun showNotificationMessage(tag: String?, id: Int, notification: Notification) { + notificationManager.notify(tag, id, notification) + } + + fun cancelNotificationMessage(tag: String?, id: Int) { + notificationManager.cancel(tag, id) + } + + fun cancelAllNotifications() { + // Keep this try catch (reported by GA) + try { + notificationManager.cancelAll() + } catch (e: Exception) { + Timber.e(e, "## cancelAllNotifications() failed") + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt new file mode 100644 index 00000000000..c40680a1fd6 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt @@ -0,0 +1,241 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.element.android.libraries.push.impl.notifications + +import android.content.Context +import android.os.Handler +import android.os.HandlerThread +import androidx.annotation.WorkerThread +import io.element.android.libraries.androidutils.throttler.FirstThrottler +import io.element.android.libraries.core.cache.CircularCache +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.push.api.store.PushDataStore +import io.element.android.libraries.push.impl.R +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreMessageEventInRoom +import timber.log.Timber +import javax.inject.Inject + +/** + * The NotificationDrawerManager receives notification events as they arrived (from event stream or fcm) and + * organise them in order to display them in the notification drawer. + * Events can be grouped into the same notification, old (already read) events can be removed to do some cleaning. + */ +@SingleIn(AppScope::class) +class NotificationDrawerManager @Inject constructor( + @ApplicationContext context: Context, + private val notificationDisplayer: NotificationDisplayer, + private val pushDataStore: PushDataStore, + // private val activeSessionDataSource: ActiveSessionDataSource, + private val notifiableEventProcessor: NotifiableEventProcessor, + private val notificationRenderer: NotificationRenderer, + private val notificationEventPersistence: NotificationEventPersistence, + private val filteredEventDetector: FilteredEventDetector, + private val buildMeta: BuildMeta, +) { + + private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY) + private var backgroundHandler: Handler + + // TODO Multi-session: this will have to be improved + /* + private val currentSession: Session? + get() = activeSessionDataSource.currentValue?.orNull() + + */ + + /** + * Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events. + */ + private val notificationState by lazy { createInitialNotificationState() } + private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) + private var currentRoomId: String? = null + private var currentThreadId: String? = null + private val firstThrottler = FirstThrottler(200) + + private var useCompleteNotificationFormat = pushDataStore.useCompleteNotificationFormat() + + init { + handlerThread.start() + backgroundHandler = Handler(handlerThread.looper) + } + + private fun createInitialNotificationState(): NotificationState { + val queuedEvents = notificationEventPersistence.loadEvents(factory = { rawEvents -> + NotificationEventQueue(rawEvents.toMutableList(), seenEventIds = CircularCache.create(cacheSize = 25)) + }) + val renderedEvents = queuedEvents.rawEvents().map { ProcessedEvent(ProcessedEvent.Type.KEEP, it) }.toMutableList() + return NotificationState(queuedEvents, renderedEvents) + } + + /** + Should be called as soon as a new event is ready to be displayed. + The notification corresponding to this event will not be displayed until + #refreshNotificationDrawer() is called. + Events might be grouped and there might not be one notification per event! + */ + fun NotificationEventQueue.onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { + if (!pushDataStore.areNotificationEnabledForDevice()) { + Timber.i("Notification are disabled for this device") + return + } + // If we support multi session, event list should be per userId + // Currently only manage single session + if (buildMeta.lowPrivacyLoggingEnabled) { + Timber.d("onNotifiableEventReceived(): $notifiableEvent") + } else { + Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}") + } + + if (filteredEventDetector.shouldBeIgnored(notifiableEvent)) { + Timber.d("onNotifiableEventReceived(): ignore the event") + return + } + + add(notifiableEvent) + } + + /** + * Clear all known events and refresh the notification drawer. + */ + fun clearAllEvents() { + updateEvents { it.clear() } + } + + /** + * Should be called when the application is currently opened and showing timeline for the given roomId. + * Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room. + */ + fun setCurrentRoom(roomId: String?) { + updateEvents { + val hasChanged = roomId != currentRoomId + currentRoomId = roomId + if (hasChanged && roomId != null) { + it.clearMessagesForRoom(roomId) + } + } + } + + /** + * Should be called when the application is currently opened and showing timeline for the given threadId. + * Used to ignore events related to that thread (no need to display notification) and clean any existing notification on this room. + */ + fun setCurrentThread(threadId: String?) { + updateEvents { + val hasChanged = threadId != currentThreadId + currentThreadId = threadId + currentRoomId?.let { roomId -> + if (hasChanged && threadId != null) { + it.clearMessagesForThread(roomId, threadId) + } + } + } + } + + fun notificationStyleChanged() { + updateEvents { + val newSettings = pushDataStore.useCompleteNotificationFormat() + if (newSettings != useCompleteNotificationFormat) { + // Settings has changed, remove all current notifications + notificationDisplayer.cancelAllNotifications() + useCompleteNotificationFormat = newSettings + } + } + } + + fun updateEvents(action: NotificationDrawerManager.(NotificationEventQueue) -> Unit) { + notificationState.updateQueuedEvents(this) { queuedEvents, _ -> + action(queuedEvents) + } + refreshNotificationDrawer() + } + + private fun refreshNotificationDrawer() { + // Implement last throttler + val canHandle = firstThrottler.canHandle() + Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms") + backgroundHandler.removeCallbacksAndMessages(null) + + backgroundHandler.postDelayed( + { + try { + refreshNotificationDrawerBg() + } catch (throwable: Throwable) { + // It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer + Timber.w(throwable, "refreshNotificationDrawerBg failure") + } + }, + canHandle.waitMillis() + ) + } + + @WorkerThread + private fun refreshNotificationDrawerBg() { + Timber.v("refreshNotificationDrawerBg()") + val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents -> + notifiableEventProcessor.process(queuedEvents.rawEvents(), currentRoomId, currentThreadId, renderedEvents).also { + queuedEvents.clearAndAdd(it.onlyKeptEvents()) + } + } + + if (notificationState.hasAlreadyRendered(eventsToRender)) { + Timber.d("Skipping notification update due to event list not changing") + } else { + notificationState.clearAndAddRenderedEvents(eventsToRender) + // TODO EAx + //val session = currentSession ?: return + //renderEvents(session, eventsToRender) + persistEvents() + } + } + + private fun persistEvents() { + notificationState.queuedEvents { queuedEvents -> + notificationEventPersistence.persistEvents(queuedEvents) + } + } + + private fun renderEvents(/*session: Session, eventsToRender: List>*/) { + /* TODO EAx + val user = session.getUserOrDefault(session.myUserId) + // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash + val myUserDisplayName = user.toMatrixItem().getBestName() + val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail( + contentUrl = user.avatarUrl, + width = avatarSize, + height = avatarSize, + method = ContentUrlResolver.ThumbnailMethod.SCALE + ) + notificationRenderer.render(session.myUserId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, eventsToRender) + + */ + } + + fun shouldIgnoreMessageEventInRoom(resolvedEvent: NotifiableMessageEvent): Boolean { + return resolvedEvent.shouldIgnoreMessageEventInRoom(currentRoomId, currentThreadId) + } + + companion object { + const val SUMMARY_NOTIFICATION_ID = 0 + const val ROOM_MESSAGES_NOTIFICATION_ID = 1 + const val ROOM_EVENT_NOTIFICATION_ID = 2 + const val ROOM_INVITATION_NOTIFICATION_ID = 3 + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt new file mode 100644 index 00000000000..c8ba481323f --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.content.Context +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import javax.inject.Inject + +// TODO Multi-account +private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache" +private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr" + +class NotificationEventPersistence @Inject constructor( + @ApplicationContext private val context: Context, + // private val matrix: Matrix, +) { + + fun loadEvents(factory: (List) -> NotificationEventQueue): NotificationEventQueue { + try { + val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) + if (file.exists()) { + file.inputStream().use { + val events: ArrayList? = null // TODO EAx matrix.secureStorageService().loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE) + if (events != null) { + return factory(events) + } + } + } + } catch (e: Throwable) { + Timber.e(e, "## Failed to load cached notification info") + } + return factory(emptyList()) + } + + fun persistEvents(queuedEvents: NotificationEventQueue) { + if (queuedEvents.isEmpty()) { + deleteCachedRoomNotifications(context) + return + } + try { + val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) + if (!file.exists()) file.createNewFile() + FileOutputStream(file).use { + // TODO EAx + // matrix.secureStorageService().securelyStoreObject(queuedEvents.rawEvents(), KEY_ALIAS_SECRET_STORAGE, it) + } + } catch (e: Throwable) { + Timber.e(e, "## Failed to save cached notification info") + } + } + + private fun deleteCachedRoomNotifications(context: Context) { + val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) + if (file.exists()) { + file.delete() + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt new file mode 100644 index 00000000000..7766ed04e8c --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import io.element.android.libraries.core.cache.CircularCache +import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent +import timber.log.Timber + +data class NotificationEventQueue( + private val queue: MutableList, + /** + * An in memory FIFO cache of the seen events. + * Acts as a notification debouncer to stop already dismissed push notifications from + * displaying again when the /sync response is delayed. + */ + private val seenEventIds: CircularCache +) { + + fun markRedacted(eventIds: List) { + eventIds.forEach { redactedId -> + queue.replace(redactedId) { + when (it) { + is InviteNotifiableEvent -> it.copy(isRedacted = true) + is NotifiableMessageEvent -> it.copy(isRedacted = true) + is SimpleNotifiableEvent -> it.copy(isRedacted = true) + } + } + } + } + + fun syncRoomEvents(roomsLeft: Collection, roomsJoined: Collection) { + if (roomsLeft.isNotEmpty() || roomsJoined.isNotEmpty()) { + queue.removeAll { + when (it) { + is NotifiableMessageEvent -> roomsLeft.contains(it.roomId) + is InviteNotifiableEvent -> roomsLeft.contains(it.roomId) || roomsJoined.contains(it.roomId) + else -> false + } + } + } + } + + fun isEmpty() = queue.isEmpty() + + fun clearAndAdd(events: List) { + queue.clear() + queue.addAll(events) + } + + fun clear() { + queue.clear() + } + + fun add(notifiableEvent: NotifiableEvent) { + val existing = findExistingById(notifiableEvent) + val edited = findEdited(notifiableEvent) + when { + existing != null -> { + if (existing.canBeReplaced) { + // Use the event coming from the event stream as it may contains more info than + // the fcm one (like type/content/clear text) (e.g when an encrypted message from + // FCM should be update with clear text after a sync) + // In this case the message has already been notified, and might have done some noise + // So we want the notification to be updated even if it has already been displayed + // Use setOnlyAlertOnce to ensure update notification does not interfere with sound + // from first notify invocation as outlined in: + // https://developer.android.com/training/notify-user/build-notification#Updating + replace(replace = existing, with = notifiableEvent) + } else { + // keep the existing one, do not replace + } + } + edited != null -> { + // Replace the existing notification with the new content + replace(replace = edited, with = notifiableEvent) + } + seenEventIds.contains(notifiableEvent.eventId) -> { + // we've already seen the event, lets skip + Timber.d("onNotifiableEventReceived(): skipping event, already seen") + } + else -> { + seenEventIds.put(notifiableEvent.eventId) + queue.add(notifiableEvent) + } + } + } + + private fun findExistingById(notifiableEvent: NotifiableEvent): NotifiableEvent? { + return queue.firstOrNull { it.eventId == notifiableEvent.eventId } + } + + private fun findEdited(notifiableEvent: NotifiableEvent): NotifiableEvent? { + return notifiableEvent.editedEventId?.let { editedId -> + queue.firstOrNull { + it.eventId == editedId || it.editedEventId == editedId + } + } + } + + private fun replace(replace: NotifiableEvent, with: NotifiableEvent) { + queue.remove(replace) + queue.add( + when (with) { + is InviteNotifiableEvent -> with.copy(isUpdated = true) + is NotifiableMessageEvent -> with.copy(isUpdated = true) + is SimpleNotifiableEvent -> with.copy(isUpdated = true) + } + ) + } + + fun clearMemberShipNotificationForRoom(roomId: String) { + Timber.d("clearMemberShipOfRoom $roomId") + queue.removeAll { it is InviteNotifiableEvent && it.roomId == roomId } + } + + fun clearMessagesForRoom(roomId: String) { + Timber.d("clearMessageEventOfRoom $roomId") + queue.removeAll { it is NotifiableMessageEvent && it.roomId == roomId } + } + + fun clearMessagesForThread(roomId: String, threadId: String) { + Timber.d("clearMessageEventOfThread $roomId, $threadId") + queue.removeAll { it is NotifiableMessageEvent && it.roomId == roomId && it.threadId == threadId } + } + + fun rawEvents(): List = queue +} + +private fun MutableList.replace(eventId: String, block: (NotifiableEvent) -> NotifiableEvent) { + val indexToReplace = indexOfFirst { it.eventId == eventId } + if (indexToReplace == -1) { + return + } + set(indexToReplace, block(get(indexToReplace))) +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt new file mode 100644 index 00000000000..f935a363667 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.app.Notification +import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent +import javax.inject.Inject + +private typealias ProcessedMessageEvents = List> + +class NotificationFactory @Inject constructor( + private val notificationUtils: NotificationUtils, + private val roomGroupMessageCreator: RoomGroupMessageCreator, + private val summaryGroupMessageCreator: SummaryGroupMessageCreator +) { + + fun Map.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List { + return map { (roomId, events) -> + when { + events.hasNoEventsToDisplay() -> RoomNotification.Removed(roomId) + else -> { + val messageEvents = events.onlyKeptEvents().filterNot { it.isRedacted } + roomGroupMessageCreator.createRoomMessage(messageEvents, roomId, myUserDisplayName, myUserAvatarUrl) + } + } + } + } + + private fun ProcessedMessageEvents.hasNoEventsToDisplay() = isEmpty() || all { + it.type == ProcessedEvent.Type.REMOVE || it.event.canNotBeDisplayed() + } + + private fun NotifiableMessageEvent.canNotBeDisplayed() = isRedacted + + @JvmName("toNotificationsInviteNotifiableEvent") + fun List>.toNotifications(myUserId: String): List { + return map { (processed, event) -> + when (processed) { + ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId) + ProcessedEvent.Type.KEEP -> OneShotNotification.Append( + notificationUtils.buildRoomInvitationNotification(event, myUserId), + OneShotNotification.Append.Meta( + key = event.roomId, + summaryLine = event.description, + isNoisy = event.noisy, + timestamp = event.timestamp + ) + ) + } + } + } + + @JvmName("toNotificationsSimpleNotifiableEvent") + fun List>.toNotifications(myUserId: String): List { + return map { (processed, event) -> + when (processed) { + ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId) + ProcessedEvent.Type.KEEP -> OneShotNotification.Append( + notificationUtils.buildSimpleEventNotification(event, myUserId), + OneShotNotification.Append.Meta( + key = event.eventId, + summaryLine = event.description, + isNoisy = event.noisy, + timestamp = event.timestamp + ) + ) + } + } + } + + fun createSummaryNotification( + roomNotifications: List, + invitationNotifications: List, + simpleNotifications: List, + useCompleteNotificationFormat: Boolean + ): SummaryNotification { + val roomMeta = roomNotifications.filterIsInstance().map { it.meta } + val invitationMeta = invitationNotifications.filterIsInstance().map { it.meta } + val simpleMeta = simpleNotifications.filterIsInstance().map { it.meta } + return when { + roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed + else -> SummaryNotification.Update( + summaryGroupMessageCreator.createSummaryNotification( + roomNotifications = roomMeta, + invitationNotifications = invitationMeta, + simpleNotifications = simpleMeta, + useCompleteNotificationFormat = useCompleteNotificationFormat + ) + ) + } + } +} + +sealed interface RoomNotification { + data class Removed(val roomId: String) : RoomNotification + data class Message(val notification: Notification, val meta: Meta) : RoomNotification { + data class Meta( + val summaryLine: CharSequence, + val messageCount: Int, + val latestTimestamp: Long, + val roomId: String, + val shouldBing: Boolean + ) + } +} + +sealed interface OneShotNotification { + data class Removed(val key: String) : OneShotNotification + data class Append(val notification: Notification, val meta: Meta) : OneShotNotification { + data class Meta( + val key: String, + val summaryLine: CharSequence, + val isNoisy: Boolean, + val timestamp: Long, + ) + } +} + +sealed interface SummaryNotification { + object Removed : SummaryNotification + data class Update(val notification: Notification) : SummaryNotification +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt new file mode 100644 index 00000000000..8b5fa703654 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.element.android.libraries.push.impl.notifications + +import androidx.annotation.WorkerThread +import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager.Companion.ROOM_EVENT_NOTIFICATION_ID +import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager.Companion.ROOM_INVITATION_NOTIFICATION_ID +import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager.Companion.ROOM_MESSAGES_NOTIFICATION_ID +import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager.Companion.SUMMARY_NOTIFICATION_ID +import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent +import timber.log.Timber +import javax.inject.Inject + +class NotificationRenderer @Inject constructor( + private val notificationDisplayer: NotificationDisplayer, + private val notificationFactory: NotificationFactory, +) { + + @WorkerThread + fun render( + myUserId: String, + myUserDisplayName: String, + myUserAvatarUrl: String?, + useCompleteNotificationFormat: Boolean, + eventsToProcess: List> + ) { + val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType() + with(notificationFactory) { + val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) + val invitationNotifications = invitationEvents.toNotifications(myUserId) + val simpleNotifications = simpleEvents.toNotifications(myUserId) + val summaryNotification = createSummaryNotification( + roomNotifications = roomNotifications, + invitationNotifications = invitationNotifications, + simpleNotifications = simpleNotifications, + useCompleteNotificationFormat = useCompleteNotificationFormat + ) + + // Remove summary first to avoid briefly displaying it after dismissing the last notification + if (summaryNotification == SummaryNotification.Removed) { + Timber.d("Removing summary notification") + notificationDisplayer.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) + } + + roomNotifications.forEach { wrapper -> + when (wrapper) { + is RoomNotification.Removed -> { + Timber.d("Removing room messages notification ${wrapper.roomId}") + notificationDisplayer.cancelNotificationMessage(wrapper.roomId, ROOM_MESSAGES_NOTIFICATION_ID) + } + is RoomNotification.Message -> if (useCompleteNotificationFormat) { + Timber.d("Updating room messages notification ${wrapper.meta.roomId}") + notificationDisplayer.showNotificationMessage(wrapper.meta.roomId, ROOM_MESSAGES_NOTIFICATION_ID, wrapper.notification) + } + } + } + + invitationNotifications.forEach { wrapper -> + when (wrapper) { + is OneShotNotification.Removed -> { + Timber.d("Removing invitation notification ${wrapper.key}") + notificationDisplayer.cancelNotificationMessage(wrapper.key, ROOM_INVITATION_NOTIFICATION_ID) + } + is OneShotNotification.Append -> if (useCompleteNotificationFormat) { + Timber.d("Updating invitation notification ${wrapper.meta.key}") + notificationDisplayer.showNotificationMessage(wrapper.meta.key, ROOM_INVITATION_NOTIFICATION_ID, wrapper.notification) + } + } + } + + simpleNotifications.forEach { wrapper -> + when (wrapper) { + is OneShotNotification.Removed -> { + Timber.d("Removing simple notification ${wrapper.key}") + notificationDisplayer.cancelNotificationMessage(wrapper.key, ROOM_EVENT_NOTIFICATION_ID) + } + is OneShotNotification.Append -> if (useCompleteNotificationFormat) { + Timber.d("Updating simple notification ${wrapper.meta.key}") + notificationDisplayer.showNotificationMessage(wrapper.meta.key, ROOM_EVENT_NOTIFICATION_ID, wrapper.notification) + } + } + } + + // Update summary last to avoid briefly displaying it before other notifications + if (summaryNotification is SummaryNotification.Update) { + Timber.d("Updating summary notification") + notificationDisplayer.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, summaryNotification.notification) + } + } + } +} + +private fun List>.groupByType(): GroupedNotificationEvents { + val roomIdToEventMap: MutableMap>> = LinkedHashMap() + val simpleEvents: MutableList> = ArrayList() + val invitationEvents: MutableList> = ArrayList() + forEach { + when (val event = it.event) { + is InviteNotifiableEvent -> invitationEvents.add(it.castedToEventType()) + is NotifiableMessageEvent -> { + val roomEvents = roomIdToEventMap.getOrPut(event.roomId) { ArrayList() } + roomEvents.add(it.castedToEventType()) + } + is SimpleNotifiableEvent -> simpleEvents.add(it.castedToEventType()) + } + } + return GroupedNotificationEvents(roomIdToEventMap, simpleEvents, invitationEvents) +} + +@Suppress("UNCHECKED_CAST") +private fun ProcessedEvent.castedToEventType(): ProcessedEvent = this as ProcessedEvent + +data class GroupedNotificationEvents( + val roomEvents: Map>>, + val simpleEvents: List>, + val invitationEvents: List> +) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt new file mode 100644 index 00000000000..808bf4114b3 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent + +class NotificationState( + /** + * The notifiable events queued for rendering or currently rendered. + * + * This is our source of truth for notifications, any changes to this list will be rendered as notifications. + * When events are removed the previously rendered notifications will be cancelled. + * When adding or updating, the notifications will be notified. + * + * Events are unique by their properties, we should be careful not to insert multiple events with the same event-id. + */ + private val queuedEvents: NotificationEventQueue, + + /** + * The last known rendered notifiable events. + * We keep track of them in order to know which events have been removed from the eventList + * allowing us to cancel any notifications previous displayed by now removed events + */ + private val renderedEvents: MutableList>, +) { + + fun updateQueuedEvents( + drawerManager: NotificationDrawerManager, + action: NotificationDrawerManager.(NotificationEventQueue, List>) -> T + ): T { + return synchronized(queuedEvents) { + action(drawerManager, queuedEvents, renderedEvents) + } + } + + fun clearAndAddRenderedEvents(eventsToRender: List>) { + renderedEvents.clear() + renderedEvents.addAll(eventsToRender) + } + + fun hasAlreadyRendered(eventsToRender: List>) = renderedEvents == eventsToRender + + fun queuedEvents(block: (NotificationEventQueue) -> Unit) { + synchronized(queuedEvents) { + block(queuedEvents) + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt new file mode 100755 index 00000000000..fe2b9ccfd81 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -0,0 +1,744 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("UNUSED_PARAMETER") + +package io.element.android.libraries.push.impl.notifications + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import androidx.core.content.getSystemService +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast +import androidx.annotation.DrawableRes +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.RemoteInput +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import io.element.android.libraries.androidutils.intent.PendingIntentCompat +import io.element.android.libraries.androidutils.system.startNotificationChannelSettingsIntent +import io.element.android.libraries.androidutils.uri.createIgnoredUri +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.push.impl.R +import io.element.android.libraries.toolbox.api.strings.StringProvider +import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent +import io.element.android.libraries.toolbox.api.systemclock.SystemClock +import timber.log.Timber +import javax.inject.Inject +import io.element.android.libraries.ui.strings.R as StringR + +// TODO EAx Split into factories +@SingleIn(AppScope::class) +class NotificationUtils @Inject constructor( + @ApplicationContext private val context: Context, + // private val vectorPreferences: VectorPreferences, + private val stringProvider: StringProvider, + private val clock: SystemClock, + private val actionIds: NotificationActionIds, + private val buildMeta: BuildMeta, +) { + + companion object { + /* ========================================================================================== + * IDs for notifications + * ========================================================================================== */ + + /** + * Identifier of the foreground notification used to keep the application alive + * when it runs in background. + * This notification, which is not removable by the end user, displays what + * the application is doing while in background. + */ + const val NOTIFICATION_ID_FOREGROUND_SERVICE = 61 + + /* ========================================================================================== + * IDs for channels + * ========================================================================================== */ + + // on devices >= android O, we need to define a channel for each notifications + private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID" + + private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID" + + const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" + private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2" + + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) + fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + + fun openSystemSettingsForSilentCategory(activity: Activity) { + startNotificationChannelSettingsIntent(activity, SILENT_NOTIFICATION_CHANNEL_ID) + } + + fun openSystemSettingsForNoisyCategory(activity: Activity) { + startNotificationChannelSettingsIntent(activity, NOISY_NOTIFICATION_CHANNEL_ID) + } + + fun openSystemSettingsForCallCategory(activity: Activity) { + startNotificationChannelSettingsIntent(activity, CALL_NOTIFICATION_CHANNEL_ID) + } + } + + private val notificationManager = NotificationManagerCompat.from(context) + + /* ========================================================================================== + * Channel names + * ========================================================================================== */ + + /** + * Create notification channels. + */ + fun createNotificationChannels() { + if (!supportNotificationChannels()) { + return + } + + val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) + + // Migration - the noisy channel was deleted and recreated when sound preference was changed (id was DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE + // + currentTimeMillis). + // Now the sound can only be change directly in system settings, so for app upgrading we are deleting this former channel + // Starting from this version the channel will not be dynamic + for (channel in notificationManager.notificationChannels) { + val channelId = channel.id + val legacyBaseName = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE" + if (channelId.startsWith(legacyBaseName)) { + notificationManager.deleteNotificationChannel(channelId) + } + } + // Migration - Remove deprecated channels + for (channelId in listOf("DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID", "CALL_NOTIFICATION_CHANNEL_ID")) { + notificationManager.getNotificationChannel(channelId)?.let { + notificationManager.deleteNotificationChannel(channelId) + } + } + + /** + * Default notification importance: shows everywhere, makes noise, but does not visually + * intrude. + */ + notificationManager.createNotificationChannel(NotificationChannel( + NOISY_NOTIFICATION_CHANNEL_ID, + stringProvider.getString(StringR.string.notification_noisy_notifications).ifEmpty { "Noisy notifications" }, + NotificationManager.IMPORTANCE_DEFAULT + ) + .apply { + description = stringProvider.getString(StringR.string.notification_noisy_notifications) + enableVibration(true) + enableLights(true) + lightColor = accentColor + }) + + /** + * Low notification importance: shows everywhere, but is not intrusive. + */ + notificationManager.createNotificationChannel(NotificationChannel( + SILENT_NOTIFICATION_CHANNEL_ID, + stringProvider.getString(StringR.string.notification_silent_notifications).ifEmpty { "Silent notifications" }, + NotificationManager.IMPORTANCE_LOW + ) + .apply { + description = stringProvider.getString(StringR.string.notification_silent_notifications) + setSound(null, null) + enableLights(true) + lightColor = accentColor + }) + + notificationManager.createNotificationChannel(NotificationChannel( + LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID, + stringProvider.getString(StringR.string.notification_listening_for_events).ifEmpty { "Listening for events" }, + NotificationManager.IMPORTANCE_MIN + ) + .apply { + description = stringProvider.getString(StringR.string.notification_listening_for_events) + setSound(null, null) + setShowBadge(false) + }) + + notificationManager.createNotificationChannel(NotificationChannel( + CALL_NOTIFICATION_CHANNEL_ID, + stringProvider.getString(StringR.string.call).ifEmpty { "Call" }, + NotificationManager.IMPORTANCE_HIGH + ) + .apply { + description = stringProvider.getString(StringR.string.call) + setSound(null, null) + enableLights(true) + lightColor = accentColor + }) + } + + fun getChannel(channelId: String): NotificationChannel? { + return notificationManager.getNotificationChannel(channelId) + } + + fun getChannelForIncomingCall(fromBg: Boolean): NotificationChannel? { + val notificationChannel = if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID + return getChannel(notificationChannel) + } + + /** + * Build a notification for a Room. + */ + fun buildMessagesListNotification( + messageStyle: NotificationCompat.MessagingStyle, + roomInfo: RoomEventGroupInfo, + threadId: String?, + largeIcon: Bitmap?, + lastMessageTimestamp: Long, + senderDisplayNameForReplyCompat: String?, + tickerText: String + ): Notification { + val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) + // Build the pending intent for when the notification is clicked + val openIntent = when { + threadId != null && + true /** TODO EAx 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 + return NotificationCompat.Builder(context, channelID) + .setOnlyAlertOnce(roomInfo.isUpdated) + .setWhen(lastMessageTimestamp) + // MESSAGING_STYLE sets title and content for API 16 and above devices. + .setStyle(messageStyle) + // A category allows groups of notifications to be ranked and filtered – per user or system settings. + // For example, alarm notifications should display before promo notifications, or message from known contact + // that can be displayed in not disturb mode if white listed (the later will need compat28.x) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + // ID of the corresponding shortcut, for conversation features under API 30+ + .setShortcutId(roomInfo.roomId) + // Title for API < 16 devices. + .setContentTitle(roomInfo.roomDisplayName) + // Content for API < 16 devices. + .setContentText(stringProvider.getString(StringR.string.notification_new_messages)) + // Number of new notifications for API <24 (M and below) devices. + .setSubText( + stringProvider.getQuantityString( + StringR.plurals.room_new_messages_notification, + messageStyle.messages.size, + messageStyle.messages.size + ) + ) + // Auto-bundling is enabled for 4 or more notifications on API 24+ (N+) + // devices and all Wear devices. But we want a custom grouping, so we specify the groupID + // TODO Group should be current user display name + .setGroup(buildMeta.applicationName) + // In order to avoid notification making sound twice (due to the summary notification) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) + .setSmallIcon(smallIcon) + // Set primary color (important for Wear 2.0 Notifications). + .setColor(accentColor) + // Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for + // 'importance' which is set in the NotificationChannel. The integers representing + // 'priority' are different from 'importance', so make sure you don't mix them. + .apply { + if (roomInfo.shouldBing) { + // Compat + priority = NotificationCompat.PRIORITY_DEFAULT + /* + vectorPreferences.getNotificationRingTone()?.let { + setSound(it) + } + */ + setLights(accentColor, 500, 500) + } else { + priority = NotificationCompat.PRIORITY_LOW + } + + // Add actions and notification intents + // Mark room as read + val markRoomReadIntent = Intent(context, NotificationBroadcastReceiver::class.java) + markRoomReadIntent.action = actionIds.markRoomRead + markRoomReadIntent.data = createIgnoredUri(roomInfo.roomId) + markRoomReadIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId) + val markRoomReadPendingIntent = PendingIntent.getBroadcast( + context, + clock.epochMillis().toInt(), + markRoomReadIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + + NotificationCompat.Action.Builder( + R.drawable.ic_material_done_all_white, + stringProvider.getString(StringR.string.action_mark_room_read), markRoomReadPendingIntent + ) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) + .setShowsUserInterface(false) + .build() + .let { addAction(it) } + + // Quick reply + if (!roomInfo.hasSmartReplyError) { + buildQuickReplyIntent(roomInfo.roomId, threadId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent -> + val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY) + .setLabel(stringProvider.getString(StringR.string.action_quick_reply)) + .build() + NotificationCompat.Action.Builder( + R.drawable.vector_notification_quick_reply, + stringProvider.getString(StringR.string.action_quick_reply), replyPendingIntent + ) + .addRemoteInput(remoteInput) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) + .setShowsUserInterface(false) + .build() + .let { addAction(it) } + } + } + + if (openIntent != null) { + setContentIntent(openIntent) + } + + if (largeIcon != null) { + setLargeIcon(largeIcon) + } + + val intent = Intent(context, NotificationBroadcastReceiver::class.java) + intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId) + intent.action = actionIds.dismissRoom + val pendingIntent = PendingIntent.getBroadcast( + context.applicationContext, + clock.epochMillis().toInt(), + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + setDeleteIntent(pendingIntent) + } + .setTicker(tickerText) + .build() + } + + fun buildRoomInvitationNotification( + inviteNotifiableEvent: InviteNotifiableEvent, + matrixId: String + ): Notification { + val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) + // Build the pending intent for when the notification is clicked + val smallIcon = R.drawable.ic_notification + val channelID = if (inviteNotifiableEvent.noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID + + return NotificationCompat.Builder(context, channelID) + .setOnlyAlertOnce(true) + .setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName) + .setContentText(inviteNotifiableEvent.description) + .setGroup(buildMeta.applicationName) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) + .setSmallIcon(smallIcon) + .setColor(accentColor) + .apply { + val roomId = inviteNotifiableEvent.roomId + // offer to type a quick reject button + val rejectIntent = Intent(context, NotificationBroadcastReceiver::class.java) + rejectIntent.action = actionIds.reject + rejectIntent.data = createIgnoredUri("$roomId&$matrixId") + rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) + val rejectIntentPendingIntent = PendingIntent.getBroadcast( + context, + clock.epochMillis().toInt(), + rejectIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + + addAction( + R.drawable.vector_notification_reject_invitation, + stringProvider.getString(StringR.string.action_reject), + rejectIntentPendingIntent + ) + + // offer to type a quick accept button + val joinIntent = Intent(context, NotificationBroadcastReceiver::class.java) + joinIntent.action = actionIds.join + joinIntent.data = createIgnoredUri("$roomId&$matrixId") + joinIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) + val joinIntentPendingIntent = PendingIntent.getBroadcast( + context, + clock.epochMillis().toInt(), + joinIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + addAction( + R.drawable.vector_notification_accept_invitation, + stringProvider.getString(StringR.string.action_join), + joinIntentPendingIntent + ) + + /* + val contentIntent = HomeActivity.newIntent( + context, + firstStartMainActivity = true, + inviteNotificationRoomId = inviteNotifiableEvent.roomId + ) + contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that + contentIntent.data = createIgnoredUri(inviteNotifiableEvent.eventId) + setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntentCompat.FLAG_IMMUTABLE)) + + */ + + if (inviteNotifiableEvent.noisy) { + // Compat + priority = NotificationCompat.PRIORITY_DEFAULT + /* + vectorPreferences.getNotificationRingTone()?.let { + setSound(it) + } + + */ + setLights(accentColor, 500, 500) + } else { + priority = NotificationCompat.PRIORITY_LOW + } + setAutoCancel(true) + } + .build() + } + + fun buildSimpleEventNotification( + simpleNotifiableEvent: SimpleNotifiableEvent, + matrixId: String + ): Notification { + val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) + // Build the pending intent for when the notification is clicked + val smallIcon = R.drawable.ic_notification + + val channelID = if (simpleNotifiableEvent.noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID + + return NotificationCompat.Builder(context, channelID) + .setOnlyAlertOnce(true) + .setContentTitle(buildMeta.applicationName) + .setContentText(simpleNotifiableEvent.description) + .setGroup(buildMeta.applicationName) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) + .setSmallIcon(smallIcon) + .setColor(accentColor) + .setAutoCancel(true) + .apply { + /* TODO EAx + val contentIntent = HomeActivity.newIntent(context, firstStartMainActivity = true) + contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that + contentIntent.data = createIgnoredUri(simpleNotifiableEvent.eventId) + setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntentCompat.FLAG_IMMUTABLE)) + */ + if (simpleNotifiableEvent.noisy) { + // Compat + priority = NotificationCompat.PRIORITY_DEFAULT + /* + vectorPreferences.getNotificationRingTone()?.let { + setSound(it) + } + + */ + setLights(accentColor, 500, 500) + } else { + priority = NotificationCompat.PRIORITY_LOW + } + setAutoCancel(true) + } + .build() + } + + private fun buildOpenRoomIntent(roomId: String): PendingIntent? { + return null + /* + val roomIntentTap = RoomDetailActivity.newIntent(context, TimelineArgs(roomId = roomId, switchToParentSpace = true), true) + roomIntentTap.action = actionIds.tapToView + // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that + roomIntentTap.data = createIgnoredUri("openRoom?$roomId") + + // Recreate the back stack + return TaskStackBuilder.create(context) + .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false)) + .addNextIntent(roomIntentTap) + .getPendingIntent( + clock.epochMillis().toInt(), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + */ + } + + private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: String?): PendingIntent? { + return null + /* + 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 { + TODO() + /* + val intent = HomeActivity.newIntent(context, firstStartMainActivity = false, clearNotification = true) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + intent.data = createIgnoredUri("tapSummary") + val mainIntent = MainActivity.getIntentWithNextIntent(context, intent) + return PendingIntent.getActivity( + context, + Random.nextInt(1000), + mainIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + */ + } + + /* + Direct reply is new in Android N, and Android already handles the UI, so the right pending intent + here will ideally be a Service/IntentService (for a long running background task) or a BroadcastReceiver, + which runs on the UI thread. It also works without unlocking, making the process really fluid for the user. + However, for Android devices running Marshmallow and below (API level 23 and below), + it will be more appropriate to use an activity. Since you have to provide your own UI. + */ + private fun buildQuickReplyIntent(roomId: String, threadId: String?, senderName: String?): PendingIntent? { + val intent: Intent + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + intent = Intent(context, NotificationBroadcastReceiver::class.java) + intent.action = actionIds.smartReply + intent.data = createIgnoredUri(roomId) + intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) + threadId?.let { + intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it) + } + + return PendingIntent.getBroadcast( + context, + clock.epochMillis().toInt(), + intent, + // PendingIntents attached to actions with remote inputs must be mutable + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_MUTABLE + ) + } else { + /* + TODO + if (!LockScreenActivity.isDisplayingALockScreenActivity()) { + // start your activity for Android M and below + val quickReplyIntent = Intent(context, LockScreenActivity::class.java) + quickReplyIntent.putExtra(LockScreenActivity.EXTRA_ROOM_ID, roomId) + quickReplyIntent.putExtra(LockScreenActivity.EXTRA_SENDER_NAME, senderName ?: "") + + // the action must be unique else the parameters are ignored + quickReplyIntent.action = QUICK_LAUNCH_ACTION + quickReplyIntent.data = createIgnoredUri($roomId") + return PendingIntent.getActivity(context, 0, quickReplyIntent, PendingIntentCompat.FLAG_IMMUTABLE) + } + */ + } + return null + } + + // // Number of new notifications for API <24 (M and below) devices. + /** + * Build the summary notification. + */ + fun buildSummaryListNotification( + style: NotificationCompat.InboxStyle?, + compatSummary: String, + noisy: Boolean, + lastMessageTimestamp: Long + ): Notification { + val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) + val smallIcon = R.drawable.ic_notification + + return NotificationCompat.Builder(context, if (noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID) + .setOnlyAlertOnce(true) + // used in compat < N, after summary is built based on child notifications + .setWhen(lastMessageTimestamp) + .setStyle(style) + .setContentTitle(buildMeta.applicationName) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .setSmallIcon(smallIcon) + // set content text to support devices running API level < 24 + .setContentText(compatSummary) + .setGroup(buildMeta.applicationName) + // set this notification as the summary for the group + .setGroupSummary(true) + .setColor(accentColor) + .apply { + if (noisy) { + // Compat + priority = NotificationCompat.PRIORITY_DEFAULT + /* + vectorPreferences.getNotificationRingTone()?.let { + setSound(it) + } + */ + setLights(accentColor, 500, 500) + } else { + // compat + priority = NotificationCompat.PRIORITY_LOW + } + } + .setContentIntent(buildOpenHomePendingIntentForSummary()) + .setDeleteIntent(getDismissSummaryPendingIntent()) + .build() + } + + private fun getDismissSummaryPendingIntent(): PendingIntent { + val intent = Intent(context, NotificationBroadcastReceiver::class.java) + intent.action = actionIds.dismissSummary + intent.data = createIgnoredUri("deleteSummary") + return PendingIntent.getBroadcast( + context.applicationContext, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + } + + fun showNotificationMessage(tag: String?, id: Int, notification: Notification) { + notificationManager.notify(tag, id, notification) + } + + fun cancelNotificationMessage(tag: String?, id: Int) { + notificationManager.cancel(tag, id) + } + + /** + * Cancel the foreground notification service. + */ + fun cancelNotificationForegroundService() { + notificationManager.cancel(NOTIFICATION_ID_FOREGROUND_SERVICE) + } + + /** + * Cancel all the notification. + */ + fun cancelAllNotifications() { + // Keep this try catch (reported by GA) + try { + notificationManager.cancelAll() + } catch (e: Exception) { + Timber.e(e, "## cancelAllNotifications() failed") + } + } + + @SuppressLint("LaunchActivityFromNotification") + fun displayDiagnosticNotification() { + val testActionIntent = Intent(context, TestNotificationReceiver::class.java) + testActionIntent.action = actionIds.diagnostic + val testPendingIntent = PendingIntent.getBroadcast( + context, + 0, + testActionIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + + notificationManager.notify( + "DIAGNOSTIC", + 888, + NotificationCompat.Builder(context, NOISY_NOTIFICATION_CHANNEL_ID) + .setContentTitle(buildMeta.applicationName) + .setContentText(stringProvider.getString(StringR.string.settings_troubleshoot_test_push_notification_content)) + .setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(getBitmap(context, R.drawable.element_logo_green)) + .setColor(ContextCompat.getColor(context, R.color.notification_accent_color)) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setCategory(NotificationCompat.CATEGORY_STATUS) + .setAutoCancel(true) + .setContentIntent(testPendingIntent) + .build() + ) + } + + private fun getBitmap(context: Context, @DrawableRes drawableRes: Int): Bitmap? { + val drawable = ResourcesCompat.getDrawable(context.resources, drawableRes, null) ?: return null + val canvas = Canvas() + val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) + canvas.setBitmap(bitmap) + drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) + drawable.draw(canvas) + return bitmap + } + + /** + * Return true it the user has enabled the do not disturb mode. + */ + fun isDoNotDisturbModeOn(): Boolean { + // We cannot use NotificationManagerCompat here. + val setting = context.getSystemService()!!.currentInterruptionFilter + + return setting == NotificationManager.INTERRUPTION_FILTER_NONE || + setting == NotificationManager.INTERRUPTION_FILTER_ALARMS + } + + /* + private fun getActionText(@StringRes stringRes: Int, @AttrRes colorRes: Int): Spannable { + return SpannableString(context.getText(stringRes)).apply { + val foregroundColorSpan = ForegroundColorSpan(ThemeUtils.getColor(context, colorRes)) + setSpan(foregroundColorSpan, 0, length, 0) + } + } + */ + + private fun ensureTitleNotEmpty(title: String?): CharSequence { + if (title.isNullOrBlank()) { + return buildMeta.applicationName + } + + return title + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/OutdatedEventDetector.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/OutdatedEventDetector.kt new file mode 100644 index 00000000000..1d82fc31e44 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/OutdatedEventDetector.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.element.android.libraries.push.impl.notifications + +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import javax.inject.Inject + +class OutdatedEventDetector @Inject constructor( + /// private val activeSessionDataSource: ActiveSessionDataSource +) { + + /** + * Returns true if the given event is outdated. + * Used to clean up notifications if a displayed message has been read on an + * other device. + */ + fun isMessageOutdated(notifiableEvent: NotifiableEvent): Boolean { + /* TODO EAx + val session = activeSessionDataSource.currentValue?.orNull() ?: return false + + if (notifiableEvent is NotifiableMessageEvent) { + val eventID = notifiableEvent.eventId + val roomID = notifiableEvent.roomId + val room = session.getRoom(roomID) ?: return false + return room.readService().isEventRead(eventID) + } + + */ + return false + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ProcessedEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ProcessedEvent.kt new file mode 100644 index 00000000000..2e91ca34671 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ProcessedEvent.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +data class ProcessedEvent( + val type: Type, + val event: T +) { + enum class Type { + KEEP, + REMOVE + } +} + +fun List>.onlyKeptEvents() = mapNotNull { processedEvent -> + processedEvent.event.takeIf { processedEvent.type == ProcessedEvent.Type.KEEP } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt new file mode 100644 index 00000000000..b09482264b8 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2018 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 io.element.android.libraries.push.impl.notifications + +/** + * Data class to hold information about a group of notifications for a room. + */ +data class RoomEventGroupInfo( + val roomId: String, + val roomDisplayName: String = "", + val isDirect: Boolean = false +) { + // An event in the list has not yet been display + var hasNewEvent: Boolean = false + + // true if at least one on the not yet displayed event is noisy + var shouldBing: Boolean = false + var customSound: String? = null + var hasSmartReplyError: Boolean = false + var isUpdated: Boolean = false +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt new file mode 100644 index 00000000000..34e8da9723c --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.graphics.Bitmap +import androidx.core.app.NotificationCompat +import androidx.core.app.Person +import io.element.android.libraries.toolbox.api.strings.StringProvider +import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import me.gujun.android.span.Span +import me.gujun.android.span.span +import timber.log.Timber +import javax.inject.Inject +import io.element.android.libraries.ui.strings.R as StringR + +class RoomGroupMessageCreator @Inject constructor( + private val bitmapLoader: NotificationBitmapLoader, + private val stringProvider: StringProvider, + private val notificationUtils: NotificationUtils +) { + + fun createRoomMessage(events: List, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message { + 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(lastKnownRoomEvent.matrixID) + .build() + ).also { + it.conversationTitle = roomName.takeIf { roomIsGroup } + it.isGroupConversation = roomIsGroup + it.addMessagesFromEvents(events) + } + + val tickerText = if (roomIsGroup) { + stringProvider.getString(StringR.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description) + } else { + stringProvider.getString(StringR.string.notification_ticker_text_dm, events.last().senderName, events.last().description) + } + + val largeBitmap = getRoomBitmap(events) + + val lastMessageTimestamp = events.last().timestamp + val smartReplyErrors = events.filter { it.isSmartReplyError() } + val messageCount = (events.size - smartReplyErrors.size) + val meta = RoomNotification.Message.Meta( + summaryLine = createRoomMessagesGroupSummaryLine(events, roomName, roomIsDirect = !roomIsGroup), + messageCount = messageCount, + latestTimestamp = lastMessageTimestamp, + roomId = roomId, + shouldBing = events.any { it.noisy } + ) + return RoomNotification.Message( + notificationUtils.buildMessagesListNotification( + style, + RoomEventGroupInfo(roomId, roomName, isDirect = !roomIsGroup).also { + it.hasSmartReplyError = smartReplyErrors.isNotEmpty() + it.shouldBing = meta.shouldBing + it.customSound = events.last().soundName + it.isUpdated = events.last().isUpdated + }, + threadId = lastKnownRoomEvent.threadId, + largeIcon = largeBitmap, + lastMessageTimestamp, + userDisplayName, + tickerText + ), + meta + ) + } + + private fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List) { + events.forEach { event -> + val senderPerson = if (event.outGoingMessage) { + null + } else { + Person.Builder() + .setName(event.senderName) + .setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath)) + .setKey(event.senderId) + .build() + } + when { + event.isSmartReplyError() -> addMessage( + stringProvider.getString(StringR.string.notification_inline_reply_failed), + event.timestamp, + senderPerson + ) + else -> { + val message = NotificationCompat.MessagingStyle.Message(event.body, event.timestamp, senderPerson).also { message -> + event.imageUri?.let { + message.setData("image/", it) + } + } + addMessage(message) + } + } + } + } + + private fun createRoomMessagesGroupSummaryLine(events: List, roomName: String, roomIsDirect: Boolean): CharSequence { + return try { + when (events.size) { + 1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDirect) + else -> { + stringProvider.getQuantityString( + StringR.plurals.notification_compat_summary_line_for_room, + events.size, + roomName, + events.size + ) + } + } + } catch (e: Throwable) { + // String not found or bad format + Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string") + roomName + } + } + + private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDirect: Boolean): Span { + return if (roomIsDirect) { + span { + span { + textStyle = "bold" + +String.format("%s: ", event.senderName) + } + +(event.description) + } + } else { + span { + span { + textStyle = "bold" + +String.format("%s: %s ", roomName, event.senderName) + } + +(event.description) + } + } + } + + private fun getRoomBitmap(events: List): Bitmap? { + // Use the last event (most recent?) + return events.lastOrNull() + ?.roomAvatarPath + ?.let { bitmapLoader.getRoomBitmap(it) } + } +} + +private fun NotifiableMessageEvent.isSmartReplyError() = outGoingMessage && outGoingMessageFailed diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt new file mode 100644 index 00000000000..864fd6c42b4 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.app.Notification +import androidx.core.app.NotificationCompat +import io.element.android.libraries.toolbox.api.strings.StringProvider +import javax.inject.Inject + +import io.element.android.libraries.ui.strings.R as StringR + +/** + * ======== Build summary notification ========= + * On Android 7.0 (API level 24) and higher, the system automatically builds a summary for + * your group using snippets of text from each notification. The user can expand this + * notification to see each separate notification. + * To support older versions, which cannot show a nested group of notifications, + * you must create an extra notification that acts as the summary. + * This appears as the only notification and the system hides all the others. + * So this summary should include a snippet from all the other notifications, + * which the user can tap to open your app. + * The behavior of the group summary may vary on some device types such as wearables. + * To ensure the best experience on all devices and versions, always include a group summary when you create a group + * https://developer.android.com/training/notify-user/group + */ +class SummaryGroupMessageCreator @Inject constructor( + private val stringProvider: StringProvider, + private val notificationUtils: NotificationUtils +) { + + fun createSummaryNotification( + roomNotifications: List, + invitationNotifications: List, + simpleNotifications: List, + useCompleteNotificationFormat: Boolean + ): Notification { + val summaryInboxStyle = NotificationCompat.InboxStyle().also { style -> + roomNotifications.forEach { style.addLine(it.summaryLine) } + invitationNotifications.forEach { style.addLine(it.summaryLine) } + simpleNotifications.forEach { style.addLine(it.summaryLine) } + } + + val summaryIsNoisy = roomNotifications.any { it.shouldBing } || + invitationNotifications.any { it.isNoisy } || + simpleNotifications.any { it.isNoisy } + + val messageCount = roomNotifications.fold(initial = 0) { acc, current -> acc + current.messageCount } + + val lastMessageTimestamp = roomNotifications.lastOrNull()?.latestTimestamp + ?: invitationNotifications.lastOrNull()?.timestamp + ?: simpleNotifications.last().timestamp + + // FIXME roomIdToEventMap.size is not correct, this is the number of rooms + val nbEvents = roomNotifications.size + simpleNotifications.size + val sumTitle = stringProvider.getQuantityString(StringR.plurals.notification_compat_summary_title, nbEvents, nbEvents) + summaryInboxStyle.setBigContentTitle(sumTitle) + // TODO get latest event? + .setSummaryText(stringProvider.getQuantityString(StringR.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) + return if (useCompleteNotificationFormat) { + notificationUtils.buildSummaryListNotification( + summaryInboxStyle, + sumTitle, + noisy = summaryIsNoisy, + lastMessageTimestamp = lastMessageTimestamp + ) + } else { + processSimpleGroupSummary( + summaryIsNoisy, + messageCount, + simpleNotifications.size, + invitationNotifications.size, + roomNotifications.size, + lastMessageTimestamp + ) + } + } + + private fun processSimpleGroupSummary( + summaryIsNoisy: Boolean, + messageEventsCount: Int, + simpleEventsCount: Int, + invitationEventsCount: Int, + roomCount: Int, + lastMessageTimestamp: Long + ): Notification { + // Add the simple events as message (?) + val messageNotificationCount = messageEventsCount + simpleEventsCount + + val privacyTitle = if (invitationEventsCount > 0) { + val invitationsStr = stringProvider.getQuantityString(StringR.plurals.notification_invitations, invitationEventsCount, invitationEventsCount) + if (messageNotificationCount > 0) { + // Invitation and message + val messageStr = stringProvider.getQuantityString( + StringR.plurals.room_new_messages_notification, + messageNotificationCount, messageNotificationCount + ) + if (roomCount > 1) { + // In several rooms + val roomStr = stringProvider.getQuantityString( + StringR.plurals.notification_unread_notified_messages_in_room_rooms, + roomCount, roomCount + ) + stringProvider.getString( + StringR.string.notification_unread_notified_messages_in_room_and_invitation, + messageStr, + roomStr, + invitationsStr + ) + } else { + // In one room + stringProvider.getString( + StringR.string.notification_unread_notified_messages_and_invitation, + messageStr, + invitationsStr + ) + } + } else { + // Only invitation + invitationsStr + } + } else { + // No invitation, only messages + val messageStr = stringProvider.getQuantityString( + StringR.plurals.room_new_messages_notification, + messageNotificationCount, messageNotificationCount + ) + if (roomCount > 1) { + // In several rooms + val roomStr = stringProvider.getQuantityString(StringR.plurals.notification_unread_notified_messages_in_room_rooms, roomCount, roomCount) + stringProvider.getString(StringR.string.notification_unread_notified_messages_in_room, messageStr, roomStr) + } else { + // In one room + messageStr + } + } + return notificationUtils.buildSummaryListNotification( + style = null, + compatSummary = privacyTitle, + noisy = summaryIsNoisy, + lastMessageTimestamp = lastMessageTimestamp + ) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiver.kt new file mode 100644 index 00000000000..42c0fe61afc --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiver.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.localbroadcastmanager.content.LocalBroadcastManager + +class TestNotificationReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + // Internal broadcast to any one interested + LocalBroadcastManager.getInstance(context).sendBroadcast(intent) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/InviteNotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/InviteNotifiableEvent.kt new file mode 100644 index 00000000000..3c49ba742fe --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/InviteNotifiableEvent.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.notifications.model + +data class InviteNotifiableEvent( + val matrixID: String?, + override val eventId: String, + override val editedEventId: String?, + override val canBeReplaced: Boolean, + val roomId: String, + val roomName: String?, + val noisy: Boolean, + val title: String, + val description: String, + val type: String?, + val timestamp: Long, + val soundName: String?, + override val isRedacted: Boolean = false, + override val isUpdated: Boolean = false +) : NotifiableEvent diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt new file mode 100644 index 00000000000..bcbf6146594 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.element.android.libraries.push.impl.notifications.model + +import java.io.Serializable + +/** + * Parent interface for all events which can be displayed as a Notification. + */ +sealed interface NotifiableEvent : Serializable { + val eventId: String + val editedEventId: String? + + // Used to know if event should be replaced with the one coming from eventstream + val canBeReplaced: Boolean + val isRedacted: Boolean + val isUpdated: Boolean +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt new file mode 100644 index 00000000000..52f3ad3be65 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.notifications.model + +import android.net.Uri + +data class NotifiableMessageEvent( + override val eventId: String, + override val editedEventId: String?, + override val canBeReplaced: Boolean, + val noisy: Boolean, + val timestamp: Long, + val senderName: String?, + val senderId: String?, + val body: String?, + // We cannot use Uri? type here, as that could trigger a + // NotSerializableException when persisting this to storage + val imageUriString: String?, + val roomId: String, + val threadId: String?, + val roomName: String?, + val roomIsDirect: Boolean = false, + val roomAvatarPath: String? = null, + val senderAvatarPath: String? = null, + val matrixID: String? = null, + val soundName: String? = null, + // This is used for >N notification, as the result of a smart reply + val outGoingMessage: Boolean = false, + val outGoingMessageFailed: Boolean = false, + override val isRedacted: Boolean = false, + override val isUpdated: Boolean = false +) : NotifiableEvent { + + val type: String = /* EventType.MESSAGE */ "m.room.message" + val description: String = body ?: "" + val title: String = senderName ?: "" + + val imageUri: Uri? + get() = imageUriString?.let { Uri.parse(it) } +} + +fun NotifiableMessageEvent.shouldIgnoreMessageEventInRoom(currentRoomId: String?, currentThreadId: String?): Boolean { + return when (currentRoomId) { + null -> false + else -> roomId == currentRoomId && threadId == currentThreadId + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt new file mode 100644 index 00000000000..e1d5c3347b6 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.notifications.model + +data class SimpleNotifiableEvent( + val matrixID: String?, + override val eventId: String, + override val editedEventId: String?, + val noisy: Boolean, + val title: String, + val description: String, + val type: String?, + val timestamp: Long, + val soundName: String?, + override var canBeReplaced: Boolean, + override val isRedacted: Boolean = false, + override val isUpdated: Boolean = false +) : NotifiableEvent diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/parser/PushParser.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/parser/PushParser.kt new file mode 100644 index 00000000000..7413264d5d2 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/parser/PushParser.kt @@ -0,0 +1,56 @@ +/* + * 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 io.element.android.libraries.push.impl.parser + +import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.push.impl.model.PushData +import io.element.android.libraries.push.impl.model.PushDataFcm +import io.element.android.libraries.push.impl.model.PushDataUnifiedPush +import io.element.android.libraries.push.impl.model.toPushData +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import javax.inject.Inject + +/** + * Parse the received data from Push. Json format are different depending on the source. + * + * Notifications received by FCM are formatted by the matrix gateway [1]. The data send to FCM is the content + * of the "notification" attribute of the json sent to the gateway [2][3]. + * On the other side, with UnifiedPush, the content of the message received is the content posted to the push + * gateway endpoint [3]. + * + * *Note*: If we want to get the same content with FCM and unifiedpush, we can do a new sygnal pusher [4]. + * + * [1] https://github.com/matrix-org/sygnal/blob/main/sygnal/gcmpushkin.py + * [2] https://github.com/matrix-org/sygnal/blob/main/sygnal/gcmpushkin.py#L366 + * [3] https://spec.matrix.org/latest/push-gateway-api/ + * [4] https://github.com/p1gp1g/sygnal/blob/unifiedpush/sygnal/upfcmpushkin.py (Not tested for a while) + */ +class PushParser @Inject constructor() { + fun parsePushDataUnifiedPush(message: ByteArray): PushData? { + return tryOrNull { Json.decodeFromString(String(message)) }?.toPushData() + } + + fun parsePushDataFcm(message: Map): PushData { + val pushDataFcm = PushDataFcm( + eventId = message["event_id"], + roomId = message["room_id"], + unread = message["unread"]?.let { tryOrNull { Integer.parseInt(it) } }, + ) + return pushDataFcm.toPushData() + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt new file mode 100644 index 00000000000..8474682ab65 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt @@ -0,0 +1,133 @@ +package io.element.android.libraries.push.impl.store + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.DefaultPreferences +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.push.api.model.BackgroundSyncMode +import io.element.android.libraries.push.api.store.PushDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +private val Context.dataStore: DataStore by preferencesDataStore(name = "push_store") + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultPushDataStore @Inject constructor( + @ApplicationContext private val context: Context, + @DefaultPreferences private val defaultPrefs: SharedPreferences, +) : PushDataStore { + private val pushCounter = intPreferencesKey("push_counter") + + override val pushCounterFlow: Flow = context.dataStore.data.map { preferences -> + preferences[pushCounter] ?: 0 + } + + suspend fun incrementPushCounter() { + context.dataStore.edit { settings -> + val currentCounterValue = settings[pushCounter] ?: 0 + settings[pushCounter] = currentCounterValue + 1 + } + } + + override fun areNotificationEnabledForDevice(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY, true) + } + + override fun setNotificationEnabledForDevice(enabled: Boolean) { + defaultPrefs.edit { + putBoolean(SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY, enabled) + } + } + + override fun backgroundSyncTimeOut(): Int { + return tryOrNull { + // The xml pref is saved as a string so use getString and parse + defaultPrefs.getString(SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, null)?.toInt() + } ?: BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS + } + + override fun setBackgroundSyncTimeout(timeInSecond: Int) { + defaultPrefs + .edit() + .putString(SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, timeInSecond.toString()) + .apply() + } + + override fun backgroundSyncDelay(): Int { + return tryOrNull { + // The xml pref is saved as a string so use getString and parse + defaultPrefs.getString(SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, null)?.toInt() + } ?: BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS + } + + override fun setBackgroundSyncDelay(timeInSecond: Int) { + defaultPrefs + .edit() + .putString(SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, timeInSecond.toString()) + .apply() + } + + override fun isBackgroundSyncEnabled(): Boolean { + return getFdroidSyncBackgroundMode() != BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED + } + + override fun setFdroidSyncBackgroundMode(mode: BackgroundSyncMode) { + defaultPrefs + .edit() + .putString(SETTINGS_FDROID_BACKGROUND_SYNC_MODE, mode.name) + .apply() + } + + override fun getFdroidSyncBackgroundMode(): BackgroundSyncMode { + return try { + val strPref = defaultPrefs + .getString(SETTINGS_FDROID_BACKGROUND_SYNC_MODE, BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY.name) + BackgroundSyncMode.values().firstOrNull { it.name == strPref } ?: BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY + } catch (e: Throwable) { + BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY + } + } + + /** + * Return true if Pin code is disabled, or if user set the settings to see full notification content. + */ + override fun useCompleteNotificationFormat(): Boolean { + return true + /* + return !useFlagPinCode() || + defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG, true) + */ + } + + companion object { + // notifications + const val SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY = "SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY" + const val SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY = "SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY" + + // background sync + const val SETTINGS_START_ON_BOOT_PREFERENCE_KEY = "SETTINGS_START_ON_BOOT_PREFERENCE_KEY" + const val SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY" + const val SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY = "SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY" + const val SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY = "SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY" + + const val SETTINGS_FDROID_BACKGROUND_SYNC_MODE = "SETTINGS_FDROID_BACKGROUND_SYNC_MODE" + const val SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY" + + const val SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG = "SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG" + + // notification method + const val SETTINGS_NOTIFICATION_METHOD_KEY = "SETTINGS_NOTIFICATION_METHOD_KEY" + } +} diff --git a/libraries/push/impl/src/main/res/drawable-xxhdpi/element_logo_green.xml b/libraries/push/impl/src/main/res/drawable-xxhdpi/element_logo_green.xml new file mode 100644 index 00000000000..e9b119c9690 --- /dev/null +++ b/libraries/push/impl/src/main/res/drawable-xxhdpi/element_logo_green.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/libraries/push/impl/src/main/res/drawable-xxhdpi/ic_material_done_all_white.png b/libraries/push/impl/src/main/res/drawable-xxhdpi/ic_material_done_all_white.png new file mode 100755 index 0000000000000000000000000000000000000000..1f3132a3f2f1ef1d4f4683e148fd351eec398569 GIT binary patch literal 398 zcmV;90df9`P)hdV05a26EH^Do>3VzkAd1j7p1$9mlKEvS8<$lOjC~rtLv7=+%qJ9CEuV( zSEHe6nx<)*rU}Pk{Y6Ypiv{D#MVl4ZDLH_jM4J@=Hz_$@(l-a-HYLYN+TsB8Q*vOm zgChx2PEAhdcR54lDCd802Fm#nXRVyS!8x)xSu_qEUO1~9dvU}=qQ|CKOfAw=Y|bSgrDIE*M32wXV#(VVG#2;9V!lm^$_XYl#W;baVj;qDlIHu6 z#5f+3ad~;LNU~q_0Fv|rqZUc#y1Jw|1DI%CxRJEgpPxkR`%a>$bk(*HW;Aqq7G$E4ZYbsNeFH$D1x3 z$C*1^e?oo3Hv!or`BIW2&`Xl@wSy60Vs>%X7Y<`jC?yd6(j|1!MnR51mbAvYrIR9v z)KmydMV>%h01XAj2BLstFsBYc_rm5B6Enay;;^EQKqp0O^2EAXh4>Sf)Qty{V0;sZ ze=_aPY%cGqBM=48%haam&x0odI!F#HPd3FgLxk!p4F^ z0kKvtu*A%$Cm^nejXMef-1Ao@G62sGb9EPGC5(?FeL3e2DFV-(KOR)46P& zEc4|Udghc7P`ubZzuRYssyCAa{r)B!$!YMJAlP;7!liSAx6>69C``S_Z??O6F-!K*pBnDXlxx8LWmcg@>B6qq(R9@8XS{daDS<>BtDXBUvB!MF zu5+uVqb5vxK}Q5+Rh(J+*y2g+QGJoEWxPkT(L1LBQA?F=Rs3WnJs~?`-1NNA)ER9~ zlpTjSK~qJawMvs?fOUe<#k0>?f(pyESpPg680hTSCJ+)EviU%p{{c4GcfsYJLD))^ zJN@`NM^QKcU9@7>bhdHVGQgpbNM0%5S3m-K8j9uzHkK`e*fxd}$`E+u(pU%C0@+V3 z-`Si-jfLogThdjseQW-~6ZFH<*~i46Qf5G^&onMg`6Anb!}?^gXE5hw*(Y#IK#bci zz>~DAS9qPy4-9Bq$JscRa7$g;Un~?p4FUwnV}rMDh>br8<{bK7{JTwe5R?+FQ^;w zs`H_XLUxW}Ly~wgh+CBpUA1W^Ylb*QTYr%v>4^lH5QH!HV^cn%R6yrvu<|U(5rjRZ z$Wrqisa+4~v#~z4(3+AK&BXjdpV0;Ay;WzWh;DV*{Z32++={K7B98DoYtP1`pqzkP zO_7r%;q6A6+13-sTOM{2*>TPObWa66>+IfgY;a_CjlBT3qe=}Gjx6ur3HTrVW{Fxt zHlGD$LX>0SkRt|NrGhuPoBESmuq;tmdoR9?LKavFGPw6=@&%O}X f5!@Uq=nBn0jKJO8Fdhew00000NkvXXu0mjfM&?#H literal 0 HcmV?d00001 diff --git a/libraries/push/impl/src/main/res/drawable-xxhdpi/vector_notification_accept_invitation.png b/libraries/push/impl/src/main/res/drawable-xxhdpi/vector_notification_accept_invitation.png new file mode 100755 index 0000000000000000000000000000000000000000..eb2be2518782887fc5a85e5e7458088b76b63198 GIT binary patch literal 473 zcmV;~0Ve*5P)n0!$1^!3wXZ3TqH33 zIbSAo5)cGI5ClOG+%o`F`=!Wvn3w@e&Ur+kL#WP#gl`PNp?PO?K-R}^Z7l9ts#c{;+-<8Y?QYL% ziW`;0IgxdWI7hOMiCgF59Lwqwx8|A~*1j5yrZ~ehFSRpH+png${phh9tc&y94YrB% z+`6kql~Uhd5>;_&2*2g3;=EU<8Yzf41q&7gL{u7si4;^EK@bE%5IXn)RHBsDDO{M4 P00000NkvXXu0mjf+Ct1x literal 0 HcmV?d00001 diff --git a/libraries/push/impl/src/main/res/drawable-xxhdpi/vector_notification_quick_reply.png b/libraries/push/impl/src/main/res/drawable-xxhdpi/vector_notification_quick_reply.png new file mode 100755 index 0000000000000000000000000000000000000000..4af4ae634b4c805b2b13c209262bba92d4ef5bdb GIT binary patch literal 269 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawu6VjQhEy=Vy==(G$SBbIuwHt% zp3_UKM>>291zL-BrWtE$WMrxY&q^|md-S)saec+%`%mT+^YR*%zN!3_;NI;JthdL` z{KCnCCY1w%imdJpA~Ld3FOd=(E`?RG0HH+`PU-POx#QYR|iePyTZek-J~XvsWJ zNp;6pnWrr{gH+WWS3OqoV4b69!Fg%ZjL8ar=LoBC`b|jln)7GMS;OOVHd`te@;#f8 z>^I4HhOyA|j6Ta%GFL2C6b3wAaO`BGUn!j9*xM|{uGv?7Zl}4O+1d4_3mOH1{$ucT L^>bP0l+XkKk9}#` literal 0 HcmV?d00001 diff --git a/libraries/push/impl/src/main/res/drawable-xxhdpi/vector_notification_reject_invitation.png b/libraries/push/impl/src/main/res/drawable-xxhdpi/vector_notification_reject_invitation.png new file mode 100755 index 0000000000000000000000000000000000000000..51b4401ca053ca8cad6e9903646709a2f44444df GIT binary patch literal 309 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw{&>1LhEy=VysgJ$~x+g}6OvsxLimbaeN-!xQItoH=3nd`|H@dF!kYp_dChV`b9AudQLRn7yg2 zv~UiON9*MGOYAy6D=_*g@;c3(5`S(Hi^X;wk8Ms*3SMP^!WTf z-E_DAd$kLMWY`Z`);MuKF9=d$?_mzLoFj7Xp~|_3ODg!(3;AT&wLoS^O+0tsvcZJ& zD(m_$dNm(!9@n+CTyDa$w$*a^-$!>qK3P{4end8+qbAwoFEAV!JYD@<);T3K0RXh> Be)#|Z literal 0 HcmV?d00001 diff --git a/libraries/push/impl/src/main/res/values/colors.xml b/libraries/push/impl/src/main/res/values/colors.xml new file mode 100644 index 00000000000..6e04238a1a9 --- /dev/null +++ b/libraries/push/impl/src/main/res/values/colors.xml @@ -0,0 +1,22 @@ + + + + + + #368BD6 + + diff --git a/libraries/push/impl/src/main/res/values/dimens.xml b/libraries/push/impl/src/main/res/values/dimens.xml new file mode 100644 index 00000000000..ce2fee2015c --- /dev/null +++ b/libraries/push/impl/src/main/res/values/dimens.xml @@ -0,0 +1,21 @@ + + + + + 50dp + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 944b17ab523..0c0e3ec2cfa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,5 @@ +import java.net.URI + /* * Copyright (c) 2022 New Vector Ltd * @@ -27,6 +29,14 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { + url = URI("https://www.jitpack.io") + content { + includeModule("com.github.UnifiedPush", "android-connector") + } + } + //noinspection JcenterRepositoryObsolete + jcenter() flatDir { dirs("libraries/matrix/libs") } From 29a56f948a25f607f9f73aa208ccbf0ec46e3d19 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Mar 2023 10:05:36 +0100 Subject: [PATCH 040/104] Push: be able to test Push Create `:libraries:network` --- .../android/libraries/push/api/PushService.kt | 2 + .../push/api/gateway/PushGatewayFailure.kt | 21 +++++++ libraries/push/impl/build.gradle.kts | 4 +- .../libraries/push/impl/DefaultPushService.kt | 5 ++ .../libraries/push/impl/PushersManager.kt | 20 +++---- .../push/impl/pushgateway/PushGatewayAPI.kt | 30 ++++++++++ .../impl/pushgateway/PushGatewayConfig.kt | 22 ++++++++ .../impl/pushgateway/PushGatewayDevice.kt | 34 +++++++++++ .../pushgateway/PushGatewayNotification.kt | 32 +++++++++++ .../impl/pushgateway/PushGatewayNotifyBody.kt | 29 ++++++++++ .../pushgateway/PushGatewayNotifyRequest.kt | 56 +++++++++++++++++++ .../pushgateway/PushGatewayNotifyResponse.kt | 26 +++++++++ 12 files changed, 269 insertions(+), 12 deletions(-) create mode 100644 libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/gateway/PushGatewayFailure.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayAPI.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayConfig.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayDevice.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotification.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyBody.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyResponse.kt diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt index 3ed22d7dae3..335ed9426c8 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt @@ -20,4 +20,6 @@ interface PushService { fun setCurrentRoom(roomId: String?) fun setCurrentThread(threadId: String?) fun notificationStyleChanged() + + suspend fun testPush() } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/gateway/PushGatewayFailure.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/gateway/PushGatewayFailure.kt new file mode 100644 index 00000000000..9e8acc4d8f6 --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/gateway/PushGatewayFailure.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.api.gateway + +sealed class PushGatewayFailure : Throwable(cause = null) { + object PusherRejected : PushGatewayFailure() +} diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 11950ad5f1a..d98be347592 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(libs.androidx.corektx) implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.lifecycle.process) + implementation(libs.network.retrofit) implementation(libs.serialization.json) implementation(projects.libraries.architecture) @@ -41,16 +42,17 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(projects.libraries.androidutils) + implementation(projects.libraries.network) implementation(projects.libraries.matrix.api) implementation(projects.libraries.push.api) implementation(projects.services.toolbox.api) - api("me.gujun.android:span:1.7") { exclude(group = "com.android.support", module = "support-annotations") } + implementation(platform(libs.google.firebase.bom)) implementation("com.google.firebase:firebase-messaging-ktx") diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt index cf7a5f377b3..bf3a2523154 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -25,6 +25,7 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultPushService @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, + private val pusherManager: PushersManager, ) : PushService { override fun setCurrentRoom(roomId: String?) { notificationDrawerManager.setCurrentRoom(roomId) @@ -37,4 +38,8 @@ class DefaultPushService @Inject constructor( override fun notificationStyleChanged() { notificationDrawerManager.notificationStyleChanged() } + + override suspend fun testPush() { + pusherManager.testPush() + } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index 2e87f360a54..c75cc6f76bd 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.push.impl import io.element.android.libraries.push.impl.config.PushConfig +import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest import io.element.android.libraries.toolbox.api.appname.AppNameProvider import java.util.UUID import javax.inject.Inject @@ -30,19 +31,17 @@ class PushersManager @Inject constructor( // private val localeProvider: LocaleProvider, private val appNameProvider: AppNameProvider, // private val getDeviceInfoUseCase: GetDeviceInfoUseCase, + private val pushGatewayNotifyRequest: PushGatewayNotifyRequest, ) { suspend fun testPush() { - /* - val currentSession = activeSessionHolder.getActiveSession() - - currentSession.pushersService().testPush( - unifiedPushHelper.getPushGateway() ?: return, - PushConfig.pusher_app_id, - unifiedPushHelper.getEndpointOrToken().orEmpty(), - TEST_EVENT_ID + pushGatewayNotifyRequest.execute( + PushGatewayNotifyRequest.Params( + url = unifiedPushHelper.getPushGateway() ?: return, + appId = PushConfig.pusher_app_id, + pushKey = unifiedPushHelper.getEndpointOrToken().orEmpty(), + eventId = TEST_EVENT_ID + ) ) - - */ } fun enqueueRegisterPusherWithFcmKey(pushKey: String): UUID { @@ -107,7 +106,6 @@ class PushersManager @Inject constructor( } */ - suspend fun unregisterEmailPusher(email: String) { // val currentSession = activeSessionHolder.getSafeActiveSession() ?: return // currentSession.pushersService().removeEmailPusher(email) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayAPI.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayAPI.kt new file mode 100644 index 00000000000..02bd7850e9e --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayAPI.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.pushgateway + + +import retrofit2.http.Body +import retrofit2.http.POST + +internal interface PushGatewayAPI { + /** + * Ask the Push Gateway to send a push to the current device. + * + * Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#post-matrix-push-v1-notify + */ + @POST(PushGatewayConfig.URI_PUSH_GATEWAY_PREFIX_PATH + "notify") + suspend fun notify(@Body body: PushGatewayNotifyBody): PushGatewayNotifyResponse +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayConfig.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayConfig.kt new file mode 100644 index 00000000000..5cd46f873d4 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayConfig.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.pushgateway + +object PushGatewayConfig { + // Push Gateway + const val URI_PUSH_GATEWAY_PREFIX_PATH = "_matrix/push/v1/" +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayDevice.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayDevice.kt new file mode 100644 index 00000000000..7adedfcfd26 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayDevice.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.pushgateway + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class PushGatewayDevice( + /** + * Required. The app_id given when the pusher was created. + */ + @SerialName("app_id") + val appId: String, + /** + * Required. The pushkey given when the pusher was created. + */ + @SerialName("pushkey") + val pushKey: String +) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotification.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotification.kt new file mode 100644 index 00000000000..b7649f68008 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotification.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.pushgateway + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class PushGatewayNotification( + @SerialName("event_id") + val eventId: String, + + /** + * Required. This is an array of devices that the notification should be sent to. + */ + @SerialName("devices") + val devices: List +) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyBody.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyBody.kt new file mode 100644 index 00000000000..ce41d2d83e9 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyBody.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.pushgateway + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class PushGatewayNotifyBody( + /** + * Required. Information about the push notification + */ + @SerialName("notification") + val notification: PushGatewayNotification +) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt new file mode 100644 index 00000000000..37e97a238c7 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.pushgateway + +import io.element.android.libraries.network.RetrofitFactory +import io.element.android.libraries.push.api.gateway.PushGatewayFailure +import javax.inject.Inject + +class PushGatewayNotifyRequest @Inject constructor( + private val retrofitFactory: RetrofitFactory, +) { + data class Params( + val url: String, + val appId: String, + val pushKey: String, + val eventId: String + ) + + suspend fun execute(params: Params) { + val sygnalApi = retrofitFactory.create( + params.url.substringBefore(PushGatewayConfig.URI_PUSH_GATEWAY_PREFIX_PATH) + ) + .create(PushGatewayAPI::class.java) + + val response = sygnalApi.notify( + PushGatewayNotifyBody( + PushGatewayNotification( + eventId = params.eventId, + devices = listOf( + PushGatewayDevice( + params.appId, + params.pushKey + ) + ) + ) + ) + ) + + if (response.rejectedPushKeys.contains(params.pushKey)) { + throw PushGatewayFailure.PusherRejected + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyResponse.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyResponse.kt new file mode 100644 index 00000000000..13d9cbad1de --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyResponse.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.pushgateway + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class PushGatewayNotifyResponse( + @SerialName("rejected") + val rejectedPushKeys: List +) From d96e44eca646ce8508524ef8b830a73d3803a0c2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Mar 2023 14:39:00 +0100 Subject: [PATCH 041/104] Add todos --- .../android/libraries/push/impl/PushersManager.kt | 9 ++++++--- .../push/impl/VectorFirebaseMessagingService.kt | 2 +- .../android/libraries/push/impl/VectorPushHandler.kt | 4 ++++ .../android/libraries/push/impl/model/PushData.kt | 2 ++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index c75cc6f76bd..e8058eab3fe 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -44,14 +44,14 @@ class PushersManager @Inject constructor( ) } - fun enqueueRegisterPusherWithFcmKey(pushKey: String): UUID { + fun enqueueRegisterPusherWithFcmKey(pushKey: String)/*: UUID*/ { return enqueueRegisterPusher(pushKey, PushConfig.pusher_http_url) } fun enqueueRegisterPusher( pushKey: String, gateway: String - ): UUID { + ) /*: UUID*/ { /* val currentSession = activeSessionHolder.getActiveSession() val pusher = createHttpPusher(pushKey, gateway) @@ -59,7 +59,10 @@ class PushersManager @Inject constructor( */ // TODO EAx - TODO() + // TODO() + // Get all sessions + // Register pusher + // Close sessions } private fun createHttpPusher( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt index 09dbf28a332..e9bccf7cddb 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt @@ -47,7 +47,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { Timber.tag(loggerTag.value).d("New Firebase token") fcmHelper.storeFcmToken(token) if ( - pushDataStore.areNotificationEnabledForDevice() && + // pushDataStore.areNotificationEnabledForDevice() && // TODO EAx activeSessionHolder.hasActiveSession() && unifiedPushHelper.isEmbeddedDistributor() ) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt index 9fad5a37bce..6a73cedf928 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt @@ -114,6 +114,10 @@ class VectorPushHandler @Inject constructor( } /* TODO EAx + - Open session + - get the event + - display the notif + val session = activeSessionHolder.getOrInitializeSession() if (session == null) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt index 9a91f1f1ac4..75bed1027b1 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt @@ -27,4 +27,6 @@ data class PushData( val eventId: String?, val roomId: String?, val unread: Int?, + + // TODO EAx Client secret ) From 7540fa23a7bfc831895c667d68075ef09f8f898f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 17 Mar 2023 13:28:57 +0100 Subject: [PATCH 042/104] Fix compilation after rebase --- libraries/push/impl/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index d98be347592..f9a265bcf78 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -37,7 +37,6 @@ dependencies { implementation(libs.serialization.json) implementation(projects.libraries.architecture) - implementation(projects.libraries.analytics.api) implementation(projects.libraries.uiStrings) implementation(projects.libraries.core) implementation(projects.libraries.di) @@ -46,6 +45,7 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.push.api) + implementation(projects.services.analytics.api) implementation(projects.services.toolbox.api) api("me.gujun.android:span:1.7") { From c366f19ae3f4ef1b37057998cd1fefcde424f282 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 20 Mar 2023 14:57:35 +0100 Subject: [PATCH 043/104] Remove manifest from api module --- .../push/api/src/main/AndroidManifest.xml | 64 ------------------- 1 file changed, 64 deletions(-) delete mode 100644 libraries/push/api/src/main/AndroidManifest.xml diff --git a/libraries/push/api/src/main/AndroidManifest.xml b/libraries/push/api/src/main/AndroidManifest.xml deleted file mode 100644 index 1d6f459d918..00000000000 --- a/libraries/push/api/src/main/AndroidManifest.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From b87d951ca6e70aa4598eeba6f20b55b9aee3dffd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 20 Mar 2023 15:50:14 +0100 Subject: [PATCH 044/104] Add BuildVersionSdkIntProvider --- .../api/sdk/BuildVersionSdkIntProvider.kt | 40 +++++++++++++++++++ .../sdk/DefaultBuildVersionSdkIntProvider.kt | 29 ++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/sdk/BuildVersionSdkIntProvider.kt create mode 100644 services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/sdk/DefaultBuildVersionSdkIntProvider.kt diff --git a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/sdk/BuildVersionSdkIntProvider.kt b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/sdk/BuildVersionSdkIntProvider.kt new file mode 100644 index 00000000000..38f2e2227c9 --- /dev/null +++ b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/sdk/BuildVersionSdkIntProvider.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 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 io.element.android.services.toolbox.api.sdk + +import androidx.annotation.ChecksSdkIntAtLeast + +interface BuildVersionSdkIntProvider { + /** + * Return the current version of the Android SDK. + */ + fun get(): Int + + /** + * Checks the if the current OS version is equal or greater than [version]. + * @return A `non-null` result if true, `null` otherwise. + */ + @ChecksSdkIntAtLeast(parameter = 0, lambda = 1) + fun whenAtLeast(version: Int, result: () -> T): T? { + return if (get() >= version) { + result() + } else null + } + + @ChecksSdkIntAtLeast(parameter = 0) + fun isAtLeast(version: Int) = get() >= version +} diff --git a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/sdk/DefaultBuildVersionSdkIntProvider.kt b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/sdk/DefaultBuildVersionSdkIntProvider.kt new file mode 100644 index 00000000000..d4ac1ec7394 --- /dev/null +++ b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/sdk/DefaultBuildVersionSdkIntProvider.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 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 io.element.android.services.toolbox.impl.sdk + +import android.os.Build +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultBuildVersionSdkIntProvider @Inject constructor() : + BuildVersionSdkIntProvider { + override fun get() = Build.VERSION.SDK_INT +} From 426bfdf588e2dc2e3132d36b3629f368647abcf8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 20 Mar 2023 16:29:33 +0100 Subject: [PATCH 045/104] Fix compilation after rebase. --- .../android/libraries/push/impl/PushersManager.kt | 3 +-- .../android/libraries/push/impl/UnifiedPushHelper.kt | 2 +- .../impl/notifications/NotifiableEventResolver.kt | 4 ++-- .../notifications/NotificationBroadcastReceiver.kt | 12 +++--------- .../push/impl/notifications/NotificationUtils.kt | 9 +++++---- .../impl/notifications/RoomGroupMessageCreator.kt | 2 +- .../impl/notifications/SummaryGroupMessageCreator.kt | 2 +- 7 files changed, 14 insertions(+), 20 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index e8058eab3fe..5525b04d4cc 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -18,8 +18,7 @@ package io.element.android.libraries.push.impl import io.element.android.libraries.push.impl.config.PushConfig import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest -import io.element.android.libraries.toolbox.api.appname.AppNameProvider -import java.util.UUID +import io.element.android.services.toolbox.api.appname.AppNameProvider import javax.inject.Inject internal const val DEFAULT_PUSHER_FILE_TAG = "mobile" diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt index 368f2e23361..958cd474be0 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt @@ -20,7 +20,7 @@ import android.content.Context import io.element.android.libraries.androidutils.system.getApplicationLabel import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.push.impl.config.PushConfig -import io.element.android.libraries.toolbox.api.strings.StringProvider +import io.element.android.services.toolbox.api.strings.StringProvider import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import org.unifiedpush.android.connector.UnifiedPush diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index 778fe20d7b1..d9c3a8a1678 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -16,10 +16,10 @@ package io.element.android.libraries.push.impl.notifications import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.toolbox.api.strings.StringProvider import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent -import io.element.android.libraries.toolbox.api.systemclock.SystemClock +import io.element.android.services.toolbox.api.strings.StringProvider +import io.element.android.services.toolbox.api.systemclock.SystemClock import javax.inject.Inject /** diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt index ea46654d88b..21a006e3f4e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt @@ -20,25 +20,19 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.core.app.RemoteInput -import io.element.android.libraries.analytics.api.AnalyticsTracker -import io.element.android.libraries.analytics.api.plan.JoinedRoom import io.element.android.libraries.architecture.bindings -import io.element.android.libraries.core.data.tryOrNull -import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent -import io.element.android.libraries.toolbox.api.systemclock.SystemClock -import kotlinx.coroutines.launch +import io.element.android.services.analytics.api.AnalyticsTracker +import io.element.android.services.toolbox.api.systemclock.SystemClock import timber.log.Timber -import java.util.UUID import javax.inject.Inject -import io.element.android.libraries.ui.strings.R as StringR - /** * Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.). */ class NotificationBroadcastReceiver : BroadcastReceiver() { @Inject lateinit var notificationDrawerManager: NotificationDrawerManager + //@Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var analyticsTracker: AnalyticsTracker @Inject lateinit var clock: SystemClock diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index fe2b9ccfd81..3c5110b67ad 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -23,7 +23,6 @@ import android.app.Activity import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager -import androidx.core.content.getSystemService import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -36,6 +35,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.RemoteInput import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService import androidx.core.content.res.ResourcesCompat import io.element.android.libraries.androidutils.intent.PendingIntentCompat import io.element.android.libraries.androidutils.system.startNotificationChannelSettingsIntent @@ -45,10 +45,10 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn import io.element.android.libraries.push.impl.R -import io.element.android.libraries.toolbox.api.strings.StringProvider import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent -import io.element.android.libraries.toolbox.api.systemclock.SystemClock +import io.element.android.services.toolbox.api.strings.StringProvider +import io.element.android.services.toolbox.api.systemclock.SystemClock import timber.log.Timber import javax.inject.Inject import io.element.android.libraries.ui.strings.R as StringR @@ -219,7 +219,8 @@ class NotificationUtils @Inject constructor( // Build the pending intent for when the notification is clicked val openIntent = when { threadId != null && - true /** TODO EAx vectorPreferences.areThreadMessagesEnabled() */ + true + /** TODO EAx vectorPreferences.areThreadMessagesEnabled() */ -> buildOpenThreadIntent(roomInfo, threadId) else -> buildOpenRoomIntent(roomInfo.roomId) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt index 34e8da9723c..2a2311e5917 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -19,8 +19,8 @@ package io.element.android.libraries.push.impl.notifications import android.graphics.Bitmap import androidx.core.app.NotificationCompat import androidx.core.app.Person -import io.element.android.libraries.toolbox.api.strings.StringProvider import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.services.toolbox.api.strings.StringProvider import me.gujun.android.span.Span import me.gujun.android.span.span import timber.log.Timber diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt index 864fd6c42b4..13598f343ad 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt @@ -18,7 +18,7 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification import androidx.core.app.NotificationCompat -import io.element.android.libraries.toolbox.api.strings.StringProvider +import io.element.android.services.toolbox.api.strings.StringProvider import javax.inject.Inject import io.element.android.libraries.ui.strings.R as StringR From 57b26127026e3d8d41a4aece9ba2fdd4ebcc6c52 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 21 Mar 2023 18:48:38 +0100 Subject: [PATCH 046/104] Add permission modules --- appnav/build.gradle.kts | 1 + features/roomlist/impl/build.gradle.kts | 2 + .../roomlist/impl/RoomListPresenter.kt | 14 ++ .../features/roomlist/impl/RoomListState.kt | 2 + .../roomlist/impl/RoomListStateProvider.kt | 4 +- .../features/roomlist/impl/RoomListView.kt | 42 ++++-- .../androidutils/system/SystemUtils.kt | 17 +++ libraries/permissions/api/build.gradle.kts | 30 ++++ .../permissions/api/PermissionsEvents.kt | 23 +++ .../permissions/api/PermissionsPresenter.kt | 23 +++ .../permissions/api/PermissionsState.kt | 29 ++++ .../permissions/api/PermissionsView.kt | 95 ++++++++++++ .../api/PermissionsViewStateProvider.kt | 38 +++++ .../android/libraries/permissions/api/Util.kt | 27 ++++ libraries/permissions/impl/build.gradle.kts | 67 +++++++++ .../impl/DefaultPermissionsPresenter.kt | 139 ++++++++++++++++++ .../impl/DefaultPermissionsStore.kt | 76 ++++++++++ .../permissions/impl/PermissionsStore.kt | 32 ++++ .../impl/DefaultPermissionsPresenterTest.kt | 45 ++++++ .../impl/InMemoryPermissionsStore.kt | 48 ++++++ libraries/push/impl/build.gradle.kts | 2 +- .../NotificationPermissionManager.kt | 68 +++++++++ .../kotlin/extension/DependencyHandleScope.kt | 2 + 23 files changed, 811 insertions(+), 15 deletions(-) create mode 100644 libraries/permissions/api/build.gradle.kts create mode 100644 libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvents.kt create mode 100644 libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsPresenter.kt create mode 100644 libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsState.kt create mode 100644 libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt create mode 100644 libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt create mode 100644 libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/Util.kt create mode 100644 libraries/permissions/impl/build.gradle.kts create mode 100644 libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt create mode 100644 libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt create mode 100644 libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/PermissionsStore.kt create mode 100644 libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt create mode 100644 libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/InMemoryPermissionsStore.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/permission/NotificationPermissionManager.kt diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 1cfcc405101..b2d2b391d82 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.push.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.matrixui) implementation(projects.libraries.uiStrings) diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index e865527b802..436961141e6 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -41,11 +41,13 @@ dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) implementation(projects.libraries.core) + implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.elementresources) + implementation(projects.libraries.permissions.api) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) implementation(projects.libraries.dateformatter.api) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index ac37dfe30d3..e77480f7c0c 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -16,6 +16,8 @@ package io.element.android.features.roomlist.impl +import android.Manifest +import android.os.Build import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -43,6 +45,8 @@ import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.ui.model.MatrixUser +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -59,6 +63,7 @@ class RoomListPresenter @Inject constructor( private val roomLastMessageFormatter: RoomLastMessageFormatter, private val sessionVerificationService: SessionVerificationService, private val snackbarDispatcher: SnackbarDispatcher, + private val permissionsPresenter: PermissionsPresenter, ) : Presenter { private val roomMembershipObserver: RoomMembershipObserver = client.roomMembershipObserver() @@ -105,12 +110,21 @@ class RoomListPresenter @Inject constructor( val snackbarMessage = handleSnackbarMessage(snackbarDispatcher) + // Ask for POST_NOTIFICATION PERMISSION on Android 13+ + val permissionsState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissionsPresenter.setParameter(Manifest.permission.POST_NOTIFICATIONS) + permissionsPresenter.present() + } else { + createDummyPostNotificationPermissionsState() + } + return RoomListState( matrixUser = matrixUser.value, roomList = filteredRoomSummaries.value, filter = filter, displayVerificationPrompt = displayVerificationPrompt, snackbarMessage = snackbarMessage, + permissionsState = permissionsState, eventSink = ::handleEvents ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index a14ef74e94e..2deb13ff2bc 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Immutable import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.matrix.ui.model.MatrixUser +import io.element.android.libraries.permissions.api.PermissionsState import kotlinx.collections.immutable.ImmutableList @Immutable @@ -29,5 +30,6 @@ data class RoomListState( val filter: String, val displayVerificationPrompt: Boolean, val snackbarMessage: SnackbarMessage?, + val permissionsState: PermissionsState, val eventSink: (RoomListEvents) -> Unit ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index d1ca647d62c..62fd918cca4 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser +import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import io.element.android.libraries.ui.strings.R as StringR @@ -40,9 +41,10 @@ internal fun aRoomListState() = RoomListState( matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("@id", "U")), roomList = aRoomListRoomSummaryList(), filter = "filter", - eventSink = {}, snackbarMessage = null, displayVerificationPrompt = false, + permissionsState = createDummyPostNotificationPermissionsState(), + eventSink = {} ) internal fun aRoomListRoomSummaryList(): ImmutableList { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index 1f49d8db0fd..9567ef94648 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -16,6 +16,7 @@ package io.element.android.features.roomlist.impl +import android.app.Activity import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -48,6 +49,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -57,6 +59,7 @@ import androidx.compose.ui.unit.dp import io.element.android.features.roomlist.impl.components.RoomListTopBar import io.element.android.features.roomlist.impl.components.RoomSummaryRow import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.libraries.androidutils.system.openAppSettingsPage import io.element.android.libraries.designsystem.ElementTextStyles import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -69,6 +72,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.coroutines.launch +import io.element.android.libraries.permissions.api.PermissionsView import io.element.android.libraries.designsystem.R as DrawableR import io.element.android.libraries.ui.strings.R as StringR @@ -81,14 +85,24 @@ fun RoomListView( onVerifyClicked: () -> Unit = {}, onCreateRoomClicked: () -> Unit = {}, ) { - RoomListContent( - state = state, - modifier = modifier, - onRoomClicked = onRoomClicked, - onOpenSettings = onOpenSettings, - onVerifyClicked = onVerifyClicked, - onCreateRoomClicked = onCreateRoomClicked, - ) + val activity = LocalContext.current as? Activity + + Box(modifier = modifier) { + RoomListContent( + state = state, + modifier = Modifier, + onRoomClicked = onRoomClicked, + onOpenSettings = onOpenSettings, + onVerifyClicked = onVerifyClicked, + onCreateRoomClicked = onCreateRoomClicked, + ) + PermissionsView( + state = state.permissionsState, + openSystemSettings = { + activity?.let { openAppSettingsPage(it, "") } + } + ) + } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @@ -197,11 +211,13 @@ fun RoomListContent( } }, snackbarHost = { - SnackbarHost (snackbarHostState) { data -> - Snackbar( - snackbarData = data, - ) - } + SnackbarHost(snackbarHostState) { data -> + Snackbar( + snackbarData = data, + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.primary + ) + } }, ) } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt index 800da0d5b3f..8f01f285450 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt @@ -121,6 +121,23 @@ fun startNotificationSettingsIntent(context: Context, activityResultLauncher: Ac activityResultLauncher.launch(intent) } +fun openAppSettingsPage( + activity: Activity, + noActivityFoundMessage: String, +) { + try { + activity.startActivity( + Intent().apply { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + data = Uri.fromParts("package", activity.packageName, null) + } + ) + } catch (activityNotFoundException: ActivityNotFoundException) { + activity.toast(noActivityFoundMessage) + } +} + /** * Shows notification system settings for the given channel id. */ diff --git a/libraries/permissions/api/build.gradle.kts b/libraries/permissions/api/build.gradle.kts new file mode 100644 index 00000000000..d86f790a440 --- /dev/null +++ b/libraries/permissions/api/build.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 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. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.libraries.permissions.api" +} + +dependencies { + implementation(projects.libraries.architecture) + + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) +} diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvents.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvents.kt new file mode 100644 index 00000000000..5267520320e --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.permissions.api + +sealed interface PermissionsEvents { + object OpenSystemDialog : PermissionsEvents + object CloseDialog : PermissionsEvents + object OpenSystemSettings : PermissionsEvents +} diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsPresenter.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsPresenter.kt new file mode 100644 index 00000000000..98519411bd6 --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsPresenter.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.permissions.api + +import io.element.android.libraries.architecture.Presenter + +interface PermissionsPresenter : Presenter { + fun setParameter(permission: String) +} diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsState.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsState.kt new file mode 100644 index 00000000000..975674820b2 --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsState.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.permissions.api + +data class PermissionsState( + // For instance Manifest.permission.POST_NOTIFICATIONS + val permission: String, + val permissionGranted: Boolean, + val shouldShowRationale: Boolean, + val showDialog: Boolean, + val permissionAlreadyAsked: Boolean, + // If true, there is no need to ask again, the system dialog will not be displayed + val permissionAlreadyDenied: Boolean, + val eventSink: (PermissionsEvents) -> Unit +) diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt new file mode 100644 index 00000000000..440b7e3e721 --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.permissions.api + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight + +@Composable +fun PermissionsView( + state: PermissionsState, + modifier: Modifier = Modifier, + openSystemSettings: () -> Unit = {}, +) { + if (state.showDialog.not()) return + + if (state.permissionGranted) { + // Notification Granted, nothing to do + } else if (state.permissionAlreadyDenied) { + // In this case, tell the user to go to the settings + ConfirmationDialog( + modifier = modifier, + title = "System", + content = "In order to let the application display notification, please grant the permission to the system settings", + submitText = "Open settings", + onSubmitClicked = { + state.eventSink.invoke(PermissionsEvents.OpenSystemSettings) + openSystemSettings() + }, + onDismiss = { state.eventSink.invoke(PermissionsEvents.CloseDialog) }, + ) + } else { + val textToShow = if (state.shouldShowRationale) { + // TODO Move to state + // If the user has denied the permission but the rationale can be shown, + // then gently explain why the app requires this permission + // permissions_rationale_msg_notification + "To be able to receive notifications, please grant the permission. Else you will not be able to be alerted if you've got new messages." + } else { + // TODO Move to state + // If it's the first time the user lands on this feature, or the user + // doesn't want to be asked again for this permission, explain that the + // permission is required + "To be able to receive notifications, please grant the permission." + } + ConfirmationDialog( + modifier = modifier, + title = "Notifications", + content = textToShow, + submitText = "Request permission", + onSubmitClicked = { + state.eventSink.invoke(PermissionsEvents.OpenSystemDialog) + }, + onCancelClicked = { + state.eventSink.invoke(PermissionsEvents.CloseDialog) + }, + onDismiss = {} + ) + } +} + +@Preview +@Composable +fun PermissionsViewLightPreview(@PreviewParameter(PermissionsViewStateProvider::class) state: PermissionsState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun PermissionsViewDarkPreview(@PreviewParameter(PermissionsViewStateProvider::class) state: PermissionsState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: PermissionsState) { + PermissionsView( + state = state, + ) +} diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt new file mode 100644 index 00000000000..5cf74aca90d --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.permissions.api + +import android.Manifest +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class PermissionsViewStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aPermissionsState(), + // Add other state here + ) +} + +fun aPermissionsState() = PermissionsState( + permission = Manifest.permission.INTERNET, + permissionGranted = false, + shouldShowRationale = false, + showDialog = true, + permissionAlreadyAsked = false, + permissionAlreadyDenied = false, + eventSink = {} +) diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/Util.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/Util.kt new file mode 100644 index 00000000000..b35ce363808 --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/Util.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.permissions.api + +fun createDummyPostNotificationPermissionsState() = PermissionsState( + permission = "Manifest.permission.POST_NOTIFICATIONS", + permissionGranted = true, + shouldShowRationale = false, + showDialog = false, + permissionAlreadyAsked = false, + permissionAlreadyDenied = false, + eventSink = { } +) diff --git a/libraries/permissions/impl/build.gradle.kts b/libraries/permissions/impl/build.gradle.kts new file mode 100644 index 00000000000..43608d36e89 --- /dev/null +++ b/libraries/permissions/impl/build.gradle.kts @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) +} + +android { + namespace = "io.element.android.libraries.permissions.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + anvil(projects.anvilcodegen) + implementation(projects.anvilannotations) + + implementation(libs.accompanist.permission) + implementation(libs.androidx.datastore.preferences) + + implementation(projects.libraries.core) + implementation(projects.libraries.androidutils) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.elementresources) + implementation(projects.libraries.uiStrings) + api(projects.libraries.permissions.api) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + + + androidTestImplementation(libs.test.junitext) + + ksp(libs.showkase.processor) +} diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt new file mode 100644 index 00000000000..db569469223 --- /dev/null +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.permissions.impl + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.permissions.api.PermissionsEvents +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.api.PermissionsState +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultPermissionsPresenter @Inject constructor( + private val permissionsStore: PermissionsStore, +) : PermissionsPresenter { + + private lateinit var permission: String + + // TODO Move to the constructor. + override fun setParameter(permission: String) { + this.permission = permission + } + + @OptIn(ExperimentalPermissionsApi::class) + @SuppressLint("InlinedApi") + @Composable + override fun present(): PermissionsState { + val localCoroutineScope = rememberCoroutineScope() + + // To reset the store: resetStore() + + val isAlreadyDenied: Boolean by permissionsStore + .isPermissionDenied(permission) + .collectAsState(initial = false) + + val isAlreadyAsked: Boolean by permissionsStore + .isPermissionAsked(permission) + .collectAsState(initial = false) + + var permissionState: PermissionState? = null + + fun onPermissionResult(result: Boolean) { + Timber.tag("PERMISSION").w("onPermissionResult: $result") + localCoroutineScope.launch { + permissionsStore.setPermissionAsked(permission, true) + } + + if (!result) { + // Should show rational true -> denied. + if (permissionState?.status?.shouldShowRationale == true) { + Timber.tag("PERMISSION").w("onPermissionResult: reset the store") + localCoroutineScope.launch { + permissionsStore.setPermissionDenied(permission, true) + } + } + } + } + + permissionState = rememberPermissionState( + permission = permission, + onPermissionResult = ::onPermissionResult + ) + + LaunchedEffect(this) { + if (permissionState.status.isGranted) { + // User may have granted permission from the settings, to reset the store regarding this permission + permissionsStore.resetPermission(permission) + } + } + + val showDialog = rememberSaveable { mutableStateOf(true) } + + fun handleEvents(event: PermissionsEvents) { + Timber.tag("PERMISSION").w("New event: $event") + when (event) { + PermissionsEvents.CloseDialog -> { + showDialog.value = false + } + PermissionsEvents.OpenSystemDialog -> { + permissionState.launchPermissionRequest() + showDialog.value = false + } + PermissionsEvents.OpenSystemSettings -> { + showDialog.value = false + } + } + } + + return PermissionsState( + permission = permissionState.permission, + permissionGranted = permissionState.status.isGranted, + shouldShowRationale = permissionState.status.shouldShowRationale, + showDialog = showDialog.value, + permissionAlreadyAsked = isAlreadyAsked, + permissionAlreadyDenied = isAlreadyDenied, + eventSink = ::handleEvents + ).also { + Timber.tag("PERMISSION").w("New state: $it") + } + } + + @Composable + private fun resetStore() { + LaunchedEffect(this@DefaultPermissionsPresenter) { + launch { + permissionsStore.resetStore() + } + } + } +} diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt new file mode 100644 index 00000000000..9ee29b7a61d --- /dev/null +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.permissions.impl + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +private val Context.dataStore: DataStore by preferencesDataStore(name = "permissions_store") + +@ContributesBinding(AppScope::class) +class DefaultPermissionsStore @Inject constructor( + @ApplicationContext context: Context, +) : PermissionsStore { + private val store = context.dataStore + + override suspend fun setPermissionDenied(permission: String, value: Boolean) { + store.edit { prefs -> + prefs[getDeniedPreferenceKey(permission)] = value + } + } + + override fun isPermissionDenied(permission: String): Flow { + return store.data.map { + it[getDeniedPreferenceKey(permission)].orFalse() + } + } + + override suspend fun setPermissionAsked(permission: String, value: Boolean) { + store.edit { prefs -> + prefs[getAskedPreferenceKey(permission)] = value + } + } + + override fun isPermissionAsked(permission: String): Flow { + return store.data.map { + it[getAskedPreferenceKey(permission)].orFalse() + } + } + + override suspend fun resetPermission(permission: String) { + setPermissionAsked(permission, false) + setPermissionDenied(permission, false) + } + + override suspend fun resetStore() { + store.edit { it.clear() } + } + + private fun getDeniedPreferenceKey(permission: String) = booleanPreferencesKey("${permission}_denied") + private fun getAskedPreferenceKey(permission: String) = booleanPreferencesKey("${permission}_asked") +} diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/PermissionsStore.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/PermissionsStore.kt new file mode 100644 index 00000000000..25b41e2a712 --- /dev/null +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/PermissionsStore.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.permissions.impl + +import kotlinx.coroutines.flow.Flow + +interface PermissionsStore { + suspend fun setPermissionDenied(permission: String, value: Boolean) + fun isPermissionDenied(permission: String): Flow + + suspend fun setPermissionAsked(permission: String, value: Boolean) + fun isPermissionAsked(permission: String): Flow + + suspend fun resetPermission(permission: String) + + // To debug + suspend fun resetStore() +} diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt new file mode 100644 index 00000000000..ba4c59b2e7c --- /dev/null +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.libraries.permissions.impl + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +const val A_PERMISSION = "A_PERMISSION" + +class DefaultPermissionsPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = DefaultPermissionsPresenter( + InMemoryPermissionsStore() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.setParameter(A_PERMISSION) + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.showDialog).isTrue() + } + } +} diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/InMemoryPermissionsStore.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/InMemoryPermissionsStore.kt new file mode 100644 index 00000000000..08a3f76130c --- /dev/null +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/InMemoryPermissionsStore.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.permissions.impl + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class InMemoryPermissionsStore( + permissionDenied: Boolean = false, + permissionAsked: Boolean = false, +) : PermissionsStore { + private val permissionDeniedFlow = MutableStateFlow(permissionDenied) + private val permissionAskedFlow = MutableStateFlow(permissionAsked) + + override suspend fun setPermissionDenied(permission: String, value: Boolean) { + permissionDeniedFlow.value = value + } + + override fun isPermissionDenied(permission: String): Flow = permissionDeniedFlow + + override suspend fun setPermissionAsked(permission: String, value: Boolean) { + permissionAskedFlow.value + } + + override fun isPermissionAsked(permission: String): Flow = permissionAskedFlow + + override suspend fun resetPermission(permission: String) { + setPermissionAsked(permission, false) + setPermissionDenied(permission, false) + } + + override suspend fun resetStore() { + } +} diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index f9a265bcf78..bb1c5bfa769 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -43,7 +43,7 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.network) implementation(projects.libraries.matrix.api) - implementation(projects.libraries.push.api) + api(projects.libraries.push.api) implementation(projects.services.analytics.api) implementation(projects.services.toolbox.api) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/permission/NotificationPermissionManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/permission/NotificationPermissionManager.kt new file mode 100644 index 00000000000..e1fd17332e1 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/permission/NotificationPermissionManager.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.permission + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import io.element.android.libraries.di.ApplicationContext +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider +import javax.inject.Inject + +// TODO EAx move +class NotificationPermissionManager @Inject constructor( + private val sdkIntProvider: BuildVersionSdkIntProvider, + @ApplicationContext private val context: Context, +) { + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + fun isPermissionGranted(): Boolean { + return ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + } + + /* + fun eventuallyRequestPermission( + activity: Activity, + requestPermissionLauncher: ActivityResultLauncher>, + showRationale: Boolean = true, + ignorePreference: Boolean = false, + ) { + if (!sdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) return + // if (!vectorPreferences.areNotificationEnabledForDevice() && !ignorePreference) return + checkPermissions( + listOf(Manifest.permission.POST_NOTIFICATIONS), + activity, + activityResultLauncher = requestPermissionLauncher, + if (showRationale) R.string.permissions_rationale_msg_notification else 0 + ) + } + */ + + fun eventuallyRevokePermission( + activity: Activity, + ) { + if (!sdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) return + activity.revokeSelfPermissionOnKill(Manifest.permission.POST_NOTIFICATIONS) + } +} diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 314421ebc85..9f0cf920994 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -73,6 +73,8 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:matrixui")) implementation(project(":libraries:network")) implementation(project(":libraries:core")) + implementation(project(":libraries:permissions:impl")) + implementation(project(":libraries:push:impl")) implementation(project(":libraries:architecture")) implementation(project(":libraries:dateformatter:impl")) implementation(project(":libraries:di")) From d882bb669076aca1d441586f239be52c904f3747 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 22 Mar 2023 10:10:19 +0100 Subject: [PATCH 047/104] Be able to test `PermissionsPresenterTest`. Create interface to abstract Accompanist implementation --- .../permissions/api/PermissionsEvents.kt | 1 - .../permissions/api/PermissionsView.kt | 2 +- .../AccompanistPermissionStateProvider.kt | 43 ++++++ .../impl/DefaultPermissionsPresenter.kt | 10 +- .../impl/DefaultPermissionsPresenterTest.kt | 145 +++++++++++++++++- .../impl/FakePermissionStateProvider.kt | 62 ++++++++ .../impl/InMemoryPermissionsStore.kt | 2 +- 7 files changed, 254 insertions(+), 11 deletions(-) create mode 100644 libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/AccompanistPermissionStateProvider.kt create mode 100644 libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvents.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvents.kt index 5267520320e..a0b2411459a 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvents.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvents.kt @@ -19,5 +19,4 @@ package io.element.android.libraries.permissions.api sealed interface PermissionsEvents { object OpenSystemDialog : PermissionsEvents object CloseDialog : PermissionsEvents - object OpenSystemSettings : PermissionsEvents } diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt index 440b7e3e721..382d8e9653d 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt @@ -42,7 +42,7 @@ fun PermissionsView( content = "In order to let the application display notification, please grant the permission to the system settings", submitText = "Open settings", onSubmitClicked = { - state.eventSink.invoke(PermissionsEvents.OpenSystemSettings) + state.eventSink.invoke(PermissionsEvents.CloseDialog) openSystemSettings() }, onDismiss = { state.eventSink.invoke(PermissionsEvents.CloseDialog) }, diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/AccompanistPermissionStateProvider.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/AccompanistPermissionStateProvider.kt new file mode 100644 index 00000000000..15acd868f28 --- /dev/null +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/AccompanistPermissionStateProvider.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalPermissionsApi::class) + +package io.element.android.libraries.permissions.impl + +import androidx.compose.runtime.Composable +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.rememberPermissionState +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +interface PermissionStateProvider { + @Composable + fun provide(permission: String, onPermissionResult: (Boolean) -> Unit): PermissionState +} + +@ContributesBinding(AppScope::class) +class AccompanistPermissionStateProvider @Inject constructor() : PermissionStateProvider { + @Composable + override fun provide(permission: String, onPermissionResult: (Boolean) -> Unit): PermissionState { + return rememberPermissionState( + permission = permission, + onPermissionResult = onPermissionResult + ) + } +} diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt index db569469223..12eb5b81fd3 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt @@ -26,8 +26,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.shouldShowRationale import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope @@ -41,6 +41,7 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultPermissionsPresenter @Inject constructor( private val permissionsStore: PermissionsStore, + private val permissionStateProvider: PermissionStateProvider, ) : PermissionsPresenter { private lateinit var permission: String @@ -85,7 +86,7 @@ class DefaultPermissionsPresenter @Inject constructor( } } - permissionState = rememberPermissionState( + permissionState = permissionStateProvider.provide( permission = permission, onPermissionResult = ::onPermissionResult ) @@ -97,7 +98,7 @@ class DefaultPermissionsPresenter @Inject constructor( } } - val showDialog = rememberSaveable { mutableStateOf(true) } + val showDialog = rememberSaveable { mutableStateOf(permissionState.status !is PermissionStatus.Granted) } fun handleEvents(event: PermissionsEvents) { Timber.tag("PERMISSION").w("New event: $event") @@ -109,9 +110,6 @@ class DefaultPermissionsPresenter @Inject constructor( permissionState.launchPermissionRequest() showDialog.value = false } - PermissionsEvents.OpenSystemSettings -> { - showDialog.value = false - } } } diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt index ba4c59b2e7c..be326a71ed8 100644 --- a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt @@ -14,14 +14,17 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) +@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalPermissionsApi::class) package io.element.android.libraries.permissions.impl import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionStatus import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.permissions.api.PermissionsEvents import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test @@ -31,8 +34,135 @@ const val A_PERMISSION = "A_PERMISSION" class DefaultPermissionsPresenterTest { @Test fun `present - initial state`() = runTest { + val permissionsStore = InMemoryPermissionsStore() + val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Granted) + val permissionStateProvider = FakePermissionStateProvider(permissionState) val presenter = DefaultPermissionsPresenter( - InMemoryPermissionsStore() + permissionsStore, + permissionStateProvider + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.setParameter(A_PERMISSION) + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.permission).isEqualTo(A_PERMISSION) + assertThat(initialState.permissionGranted).isTrue() + assertThat(initialState.shouldShowRationale).isFalse() + assertThat(initialState.permissionAlreadyAsked).isFalse() + assertThat(initialState.permissionAlreadyDenied).isFalse() + assertThat(initialState.showDialog).isFalse() + } + } + + @Test + fun `present - user closes dialog`() = runTest { + val permissionsStore = InMemoryPermissionsStore() + val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false)) + val permissionStateProvider = FakePermissionStateProvider(permissionState) + val presenter = DefaultPermissionsPresenter( + permissionsStore, + permissionStateProvider + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.setParameter(A_PERMISSION) + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.showDialog).isTrue() + initialState.eventSink.invoke(PermissionsEvents.CloseDialog) + assertThat(awaitItem().showDialog).isFalse() + } + } + + @Test + fun `present - user does not grant permission`() = runTest { + val permissionsStore = InMemoryPermissionsStore() + val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false)) + val permissionStateProvider = FakePermissionStateProvider(permissionState) + val presenter = DefaultPermissionsPresenter( + permissionsStore, + permissionStateProvider + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.setParameter(A_PERMISSION) + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.showDialog).isTrue() + initialState.eventSink.invoke(PermissionsEvents.OpenSystemDialog) + assertThat(permissionState.launchPermissionRequestCalled).isTrue() + assertThat(awaitItem().showDialog).isFalse() + // User does not grant permission + permissionStateProvider.userGiveAnswer(answer = false, firstTime = true) + skipItems(1) + val state = awaitItem() + assertThat(state.permissionGranted).isFalse() + assertThat(state.showDialog).isFalse() + assertThat(state.permissionAlreadyDenied).isFalse() + assertThat(state.permissionAlreadyAsked).isTrue() + } + } + + @Test + fun `present - user does not grant permission second time`() = runTest { + val permissionsStore = InMemoryPermissionsStore() + val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = true)) + val permissionStateProvider = FakePermissionStateProvider(permissionState) + val presenter = DefaultPermissionsPresenter( + permissionsStore, + permissionStateProvider + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.setParameter(A_PERMISSION) + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.showDialog).isTrue() + initialState.eventSink.invoke(PermissionsEvents.OpenSystemDialog) + assertThat(permissionState.launchPermissionRequestCalled).isTrue() + assertThat(awaitItem().showDialog).isFalse() + // User does not grant permission + permissionStateProvider.userGiveAnswer(answer = false, firstTime = false) + skipItems(2) + val state = awaitItem() + assertThat(state.permissionGranted).isFalse() + assertThat(state.showDialog).isFalse() + assertThat(state.permissionAlreadyDenied).isTrue() + assertThat(state.permissionAlreadyAsked).isTrue() + } + } + + @Test + fun `present - user does not grant permission third time`() = runTest { + val permissionsStore = InMemoryPermissionsStore(permissionDenied = true, permissionAsked = true) + val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false)) + val permissionStateProvider = FakePermissionStateProvider(permissionState) + val presenter = DefaultPermissionsPresenter( + permissionsStore, + permissionStateProvider + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.setParameter(A_PERMISSION) + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.showDialog).isTrue() + assertThat(initialState.permissionGranted).isFalse() + assertThat(initialState.permissionAlreadyDenied).isTrue() + assertThat(initialState.permissionAlreadyAsked).isTrue() + } + } + + @Test + fun `present - user grants permission`() = runTest { + val permissionsStore = InMemoryPermissionsStore() + val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false)) + val permissionStateProvider = FakePermissionStateProvider(permissionState) + val presenter = DefaultPermissionsPresenter( + permissionsStore, + permissionStateProvider ) moleculeFlow(RecompositionClock.Immediate) { presenter.setParameter(A_PERMISSION) @@ -40,6 +170,17 @@ class DefaultPermissionsPresenterTest { }.test { val initialState = awaitItem() assertThat(initialState.showDialog).isTrue() + initialState.eventSink.invoke(PermissionsEvents.OpenSystemDialog) + assertThat(permissionState.launchPermissionRequestCalled).isTrue() + assertThat(awaitItem().showDialog).isFalse() + // User grants permission + permissionStateProvider.userGiveAnswer(answer = true, firstTime = true) + skipItems(1) + val state = awaitItem() + assertThat(state.permissionGranted).isTrue() + assertThat(state.showDialog).isFalse() + assertThat(state.permissionAlreadyDenied).isFalse() + assertThat(state.permissionAlreadyAsked).isTrue() } } } diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt new file mode 100644 index 00000000000..2c670618114 --- /dev/null +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalPermissionsApi::class) + +package io.element.android.libraries.permissions.impl + +import androidx.compose.runtime.* +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.PermissionStatus + +class FakePermissionStateProvider constructor( + private val permissionState: FakePermissionState +) : PermissionStateProvider { + private lateinit var onPermissionResult: (Boolean) -> Unit + + @OptIn(ExperimentalPermissionsApi::class) + @Composable + override fun provide(permission: String, onPermissionResult: (Boolean) -> Unit): PermissionState { + this.onPermissionResult = onPermissionResult + return permissionState + } + + fun userGiveAnswer(answer: Boolean, firstTime: Boolean) { + onPermissionResult.invoke(answer) + permissionState.givenPermissionStatus(answer, firstTime) + } +} + +@Stable +class FakePermissionState( + override val permission: String, + initialStatus: PermissionStatus, +) : PermissionState { + + override var status: PermissionStatus by mutableStateOf(initialStatus) + + var launchPermissionRequestCalled = false + private set + + override fun launchPermissionRequest() { + launchPermissionRequestCalled = true + } + + fun givenPermissionStatus(hasPermission: Boolean, shouldShowRationale: Boolean) { + status = if (hasPermission) PermissionStatus.Granted else PermissionStatus.Denied(shouldShowRationale) + } +} diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/InMemoryPermissionsStore.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/InMemoryPermissionsStore.kt index 08a3f76130c..3f5d925ccd4 100644 --- a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/InMemoryPermissionsStore.kt +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/InMemoryPermissionsStore.kt @@ -33,7 +33,7 @@ class InMemoryPermissionsStore( override fun isPermissionDenied(permission: String): Flow = permissionDeniedFlow override suspend fun setPermissionAsked(permission: String, value: Boolean) { - permissionAskedFlow.value + permissionAskedFlow.value = value } override fun isPermissionAsked(permission: String): Flow = permissionAskedFlow From 82bb3936b4861547082894ef0f743d523eb1ed1b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 22 Mar 2023 11:38:46 +0100 Subject: [PATCH 048/104] Create noop version for the minimal sample and test. --- features/roomlist/impl/build.gradle.kts | 1 + .../roomlist/impl/RoomListPresenterTests.kt | 8 ++++ libraries/permissions/noop/build.gradle.kts | 30 ++++++++++++++ .../noop/NoopPermissionsPresenter.kt | 39 +++++++++++++++++++ samples/minimal/build.gradle.kts | 1 + .../android/samples/minimal/RoomListScreen.kt | 3 ++ 6 files changed, 82 insertions(+) create mode 100644 libraries/permissions/noop/build.gradle.kts create mode 100644 libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenter.kt diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index 436961141e6..79db4d53609 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { testImplementation(libs.test.robolectric) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.dateformatter.test) + testImplementation(projects.libraries.permissions.noop) androidTestImplementation(libs.test.junitext) } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index 3f3e43e2e73..dc16984e53a 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -37,6 +37,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter import kotlinx.coroutines.test.runTest import org.junit.Test @@ -50,6 +51,7 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), + NoopPermissionsPresenter(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -77,6 +79,7 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), + NoopPermissionsPresenter(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -98,6 +101,7 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), + NoopPermissionsPresenter(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -123,6 +127,7 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), + NoopPermissionsPresenter(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -153,6 +158,7 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), + NoopPermissionsPresenter(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -188,6 +194,7 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), + NoopPermissionsPresenter(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -237,6 +244,7 @@ class RoomListPresenterTests { givenVerifiedStatus(SessionVerifiedStatus.NotVerified) }, SnackbarDispatcher(), + NoopPermissionsPresenter(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() diff --git a/libraries/permissions/noop/build.gradle.kts b/libraries/permissions/noop/build.gradle.kts new file mode 100644 index 00000000000..7319f73104b --- /dev/null +++ b/libraries/permissions/noop/build.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 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. + */ + +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.libraries.permissions.noop" +} + +dependencies { + implementation(projects.libraries.architecture) + api(projects.libraries.permissions.api) +} diff --git a/libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenter.kt b/libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenter.kt new file mode 100644 index 00000000000..293edb2cac2 --- /dev/null +++ b/libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenter.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.permissions.noop + +import androidx.compose.runtime.Composable +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.api.PermissionsState + +class NoopPermissionsPresenter: PermissionsPresenter { + + override fun setParameter(permission: String) = Unit + + @Composable + override fun present(): PermissionsState { + return PermissionsState( + permission = "", + permissionGranted = false, + shouldShowRationale = false, + showDialog = false, + permissionAlreadyAsked = false, + permissionAlreadyDenied = false, + eventSink = {}, + ) + } +} diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 9ad0df0c0c3..8ad1c125d70 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.impl) + implementation(projects.libraries.permissions.noop) implementation(projects.libraries.sessionStorage.implMemory) implementation(projects.libraries.designsystem) implementation(projects.libraries.architecture) diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 5dce2feafe3..7d106630271 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone @@ -44,12 +45,14 @@ class RoomListScreen( private val dateTimeProvider = LocalDateTimeProvider(clock, timeZone) private val dateFormatters = DateFormatters(locale, clock, timeZone) private val sessionVerificationService = matrixClient.sessionVerificationService() + private val permissionsPresenter = NoopPermissionsPresenter() private val presenter = RoomListPresenter( matrixClient, DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters), DefaultRoomLastMessageFormatter(context, matrixClient), sessionVerificationService, SnackbarDispatcher(), + permissionsPresenter, ) @Composable From 1ae70e9bcb069602e6e9338fb9172c9c0aabbcdf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 22 Mar 2023 12:04:22 +0100 Subject: [PATCH 049/104] Use presenter factory --- features/roomlist/impl/build.gradle.kts | 1 + .../roomlist/impl/RoomListPresenter.kt | 21 +++++++++-------- .../roomlist/impl/RoomListPresenterTests.kt | 16 ++++++------- .../permissions/api/PermissionsPresenter.kt | 5 +++- .../impl/DefaultPermissionsPresenter.kt | 17 +++++++------- .../impl/DefaultPermissionsPresenterTest.kt | 12 +++++----- .../noop/NoopPermissionsPresenter.kt | 4 +--- .../noop/NoopPermissionsPresenterFactory.kt | 23 +++++++++++++++++++ .../android/samples/minimal/RoomListScreen.kt | 6 ++--- 9 files changed, 67 insertions(+), 38 deletions(-) create mode 100644 libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterFactory.kt diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index 79db4d53609..b679c2d8237 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.elementresources) implementation(projects.libraries.permissions.api) + implementation(projects.libraries.permissions.noop) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) implementation(projects.libraries.dateformatter.api) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index e77480f7c0c..c38120161bc 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -46,7 +46,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.permissions.api.PermissionsPresenter -import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState +import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -63,11 +63,20 @@ class RoomListPresenter @Inject constructor( private val roomLastMessageFormatter: RoomLastMessageFormatter, private val sessionVerificationService: SessionVerificationService, private val snackbarDispatcher: SnackbarDispatcher, - private val permissionsPresenter: PermissionsPresenter, + private val permissionsPresenterFactory: PermissionsPresenter.Factory, ) : Presenter { private val roomMembershipObserver: RoomMembershipObserver = client.roomMembershipObserver() + private val postNotificationPermissionsPresenter by lazy { + // Ask for POST_NOTIFICATION PERMISSION on Android 13+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissionsPresenterFactory.create(Manifest.permission.POST_NOTIFICATIONS) + } else { + NoopPermissionsPresenter() + } + } + @Composable override fun present(): RoomListState { val matrixUser: MutableState = remember { @@ -110,13 +119,7 @@ class RoomListPresenter @Inject constructor( val snackbarMessage = handleSnackbarMessage(snackbarDispatcher) - // Ask for POST_NOTIFICATION PERMISSION on Android 13+ - val permissionsState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - permissionsPresenter.setParameter(Manifest.permission.POST_NOTIFICATIONS) - permissionsPresenter.present() - } else { - createDummyPostNotificationPermissionsState() - } + val permissionsState = postNotificationPermissionsPresenter.present() return RoomListState( matrixUser = matrixUser.value, diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index dc16984e53a..18ead8f3e69 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -37,7 +37,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService -import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter +import io.element.android.libraries.permissions.noop.NoopPermissionsPresenterFactory import kotlinx.coroutines.test.runTest import org.junit.Test @@ -51,7 +51,7 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), - NoopPermissionsPresenter(), + NoopPermissionsPresenterFactory(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -79,7 +79,7 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), - NoopPermissionsPresenter(), + NoopPermissionsPresenterFactory(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -101,7 +101,7 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), - NoopPermissionsPresenter(), + NoopPermissionsPresenterFactory(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -127,7 +127,7 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), - NoopPermissionsPresenter(), + NoopPermissionsPresenterFactory(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -158,7 +158,7 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), - NoopPermissionsPresenter(), + NoopPermissionsPresenterFactory(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -194,7 +194,7 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), - NoopPermissionsPresenter(), + NoopPermissionsPresenterFactory(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -244,7 +244,7 @@ class RoomListPresenterTests { givenVerifiedStatus(SessionVerifiedStatus.NotVerified) }, SnackbarDispatcher(), - NoopPermissionsPresenter(), + NoopPermissionsPresenterFactory(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsPresenter.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsPresenter.kt index 98519411bd6..c4ab065ca0c 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsPresenter.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsPresenter.kt @@ -19,5 +19,8 @@ package io.element.android.libraries.permissions.api import io.element.android.libraries.architecture.Presenter interface PermissionsPresenter : Presenter { - fun setParameter(permission: String) + + interface Factory { + fun create(permission: String): PermissionsPresenter + } } diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt index 12eb5b81fd3..c09a595783f 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt @@ -30,25 +30,26 @@ import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.shouldShowRationale import com.squareup.anvil.annotations.ContributesBinding +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import io.element.android.libraries.di.AppScope import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsState import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject -@ContributesBinding(AppScope::class) -class DefaultPermissionsPresenter @Inject constructor( +class DefaultPermissionsPresenter @AssistedInject constructor( + @Assisted val permission: String, private val permissionsStore: PermissionsStore, private val permissionStateProvider: PermissionStateProvider, ) : PermissionsPresenter { - private lateinit var permission: String - - // TODO Move to the constructor. - override fun setParameter(permission: String) { - this.permission = permission + @AssistedFactory + @ContributesBinding(AppScope::class) + interface Factory : PermissionsPresenter.Factory { + override fun create(permission: String): DefaultPermissionsPresenter } @OptIn(ExperimentalPermissionsApi::class) diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt index be326a71ed8..10e6edf3f55 100644 --- a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt @@ -38,11 +38,11 @@ class DefaultPermissionsPresenterTest { val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Granted) val permissionStateProvider = FakePermissionStateProvider(permissionState) val presenter = DefaultPermissionsPresenter( + A_PERMISSION, permissionsStore, permissionStateProvider ) moleculeFlow(RecompositionClock.Immediate) { - presenter.setParameter(A_PERMISSION) presenter.present() }.test { val initialState = awaitItem() @@ -61,11 +61,11 @@ class DefaultPermissionsPresenterTest { val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false)) val permissionStateProvider = FakePermissionStateProvider(permissionState) val presenter = DefaultPermissionsPresenter( + A_PERMISSION, permissionsStore, permissionStateProvider ) moleculeFlow(RecompositionClock.Immediate) { - presenter.setParameter(A_PERMISSION) presenter.present() }.test { val initialState = awaitItem() @@ -81,11 +81,11 @@ class DefaultPermissionsPresenterTest { val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false)) val permissionStateProvider = FakePermissionStateProvider(permissionState) val presenter = DefaultPermissionsPresenter( + A_PERMISSION, permissionsStore, permissionStateProvider ) moleculeFlow(RecompositionClock.Immediate) { - presenter.setParameter(A_PERMISSION) presenter.present() }.test { val initialState = awaitItem() @@ -110,11 +110,11 @@ class DefaultPermissionsPresenterTest { val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = true)) val permissionStateProvider = FakePermissionStateProvider(permissionState) val presenter = DefaultPermissionsPresenter( + A_PERMISSION, permissionsStore, permissionStateProvider ) moleculeFlow(RecompositionClock.Immediate) { - presenter.setParameter(A_PERMISSION) presenter.present() }.test { val initialState = awaitItem() @@ -139,11 +139,11 @@ class DefaultPermissionsPresenterTest { val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false)) val permissionStateProvider = FakePermissionStateProvider(permissionState) val presenter = DefaultPermissionsPresenter( + A_PERMISSION, permissionsStore, permissionStateProvider ) moleculeFlow(RecompositionClock.Immediate) { - presenter.setParameter(A_PERMISSION) presenter.present() }.test { skipItems(1) @@ -161,11 +161,11 @@ class DefaultPermissionsPresenterTest { val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false)) val permissionStateProvider = FakePermissionStateProvider(permissionState) val presenter = DefaultPermissionsPresenter( + A_PERMISSION, permissionsStore, permissionStateProvider ) moleculeFlow(RecompositionClock.Immediate) { - presenter.setParameter(A_PERMISSION) presenter.present() }.test { val initialState = awaitItem() diff --git a/libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenter.kt b/libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenter.kt index 293edb2cac2..653fe49268f 100644 --- a/libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenter.kt +++ b/libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenter.kt @@ -20,9 +20,7 @@ import androidx.compose.runtime.Composable import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsState -class NoopPermissionsPresenter: PermissionsPresenter { - - override fun setParameter(permission: String) = Unit +class NoopPermissionsPresenter : PermissionsPresenter { @Composable override fun present(): PermissionsState { diff --git a/libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterFactory.kt b/libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterFactory.kt new file mode 100644 index 00000000000..b9829694831 --- /dev/null +++ b/libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterFactory.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.permissions.noop + +import io.element.android.libraries.permissions.api.PermissionsPresenter + +class NoopPermissionsPresenterFactory : PermissionsPresenter.Factory { + override fun create(permission: String) = NoopPermissionsPresenter() +} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 7d106630271..9d1c7a495d4 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -29,7 +29,7 @@ import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter +import io.element.android.libraries.permissions.noop.NoopPermissionsPresenterFactory import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone @@ -45,14 +45,14 @@ class RoomListScreen( private val dateTimeProvider = LocalDateTimeProvider(clock, timeZone) private val dateFormatters = DateFormatters(locale, clock, timeZone) private val sessionVerificationService = matrixClient.sessionVerificationService() - private val permissionsPresenter = NoopPermissionsPresenter() + private val permissionsPresenterFactory = NoopPermissionsPresenterFactory() private val presenter = RoomListPresenter( matrixClient, DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters), DefaultRoomLastMessageFormatter(context, matrixClient), sessionVerificationService, SnackbarDispatcher(), - permissionsPresenter, + permissionsPresenterFactory, ) @Composable From 95720d039bdd14277edbceac893a3a1580b7bdf6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Mar 2023 16:19:22 +0200 Subject: [PATCH 050/104] Temporary import strings. --- .../libraries/push/impl/GoogleFcmHelper.kt | 3 +- .../libraries/push/impl/UnifiedPushHelper.kt | 4 +- .../impl/notifications/NotificationUtils.kt | 28 ++++----- .../notifications/RoomGroupMessageCreator.kt | 10 +-- .../SummaryGroupMessageCreator.kt | 23 ++++--- .../impl/src/main/res/values/temporary.xml | 62 +++++++++++++++++++ 6 files changed, 95 insertions(+), 35 deletions(-) create mode 100644 libraries/push/impl/src/main/res/values/temporary.xml diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt index 3d602aeb9bd..16ce8d73ab8 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt @@ -28,7 +28,6 @@ import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.DefaultPreferences import timber.log.Timber import javax.inject.Inject -import io.element.android.libraries.ui.strings.R as StringR /** * This class store the FCM token in SharedPrefs and ensure this token is retrieved. @@ -69,7 +68,7 @@ class GoogleFcmHelper @Inject constructor( Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") } } else { - Toast.makeText(context, StringR.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/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt index 958cd474be0..a6b50a58dd9 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt @@ -134,8 +134,8 @@ class UnifiedPushHelper @Inject constructor( fun getCurrentDistributorName(): String { return when { - isEmbeddedDistributor() -> stringProvider.getString(StringR.string.unifiedpush_distributor_fcm_fallback) - isBackgroundSync() -> stringProvider.getString(StringR.string.unifiedpush_distributor_background_sync) + isEmbeddedDistributor() -> stringProvider.getString(R.string.unifiedpush_distributor_fcm_fallback) + isBackgroundSync() -> stringProvider.getString(R.string.unifiedpush_distributor_background_sync) else -> context.getApplicationLabel(UnifiedPush.getDistributor(context)) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index 3c5110b67ad..bfa80908bf8 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -145,11 +145,11 @@ class NotificationUtils @Inject constructor( */ notificationManager.createNotificationChannel(NotificationChannel( NOISY_NOTIFICATION_CHANNEL_ID, - stringProvider.getString(StringR.string.notification_noisy_notifications).ifEmpty { "Noisy notifications" }, + stringProvider.getString(R.string.notification_noisy_notifications).ifEmpty { "Noisy notifications" }, NotificationManager.IMPORTANCE_DEFAULT ) .apply { - description = stringProvider.getString(StringR.string.notification_noisy_notifications) + description = stringProvider.getString(R.string.notification_noisy_notifications) enableVibration(true) enableLights(true) lightColor = accentColor @@ -160,11 +160,11 @@ class NotificationUtils @Inject constructor( */ notificationManager.createNotificationChannel(NotificationChannel( SILENT_NOTIFICATION_CHANNEL_ID, - stringProvider.getString(StringR.string.notification_silent_notifications).ifEmpty { "Silent notifications" }, + stringProvider.getString(R.string.notification_silent_notifications).ifEmpty { "Silent notifications" }, NotificationManager.IMPORTANCE_LOW ) .apply { - description = stringProvider.getString(StringR.string.notification_silent_notifications) + description = stringProvider.getString(R.string.notification_silent_notifications) setSound(null, null) enableLights(true) lightColor = accentColor @@ -172,22 +172,22 @@ class NotificationUtils @Inject constructor( notificationManager.createNotificationChannel(NotificationChannel( LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID, - stringProvider.getString(StringR.string.notification_listening_for_events).ifEmpty { "Listening for events" }, + stringProvider.getString(R.string.notification_listening_for_events).ifEmpty { "Listening for events" }, NotificationManager.IMPORTANCE_MIN ) .apply { - description = stringProvider.getString(StringR.string.notification_listening_for_events) + description = stringProvider.getString(R.string.notification_listening_for_events) setSound(null, null) setShowBadge(false) }) notificationManager.createNotificationChannel(NotificationChannel( CALL_NOTIFICATION_CHANNEL_ID, - stringProvider.getString(StringR.string.call).ifEmpty { "Call" }, + stringProvider.getString(R.string.call).ifEmpty { "Call" }, NotificationManager.IMPORTANCE_HIGH ) .apply { - description = stringProvider.getString(StringR.string.call) + description = stringProvider.getString(R.string.call) setSound(null, null) enableLights(true) lightColor = accentColor @@ -242,11 +242,11 @@ class NotificationUtils @Inject constructor( // Title for API < 16 devices. .setContentTitle(roomInfo.roomDisplayName) // Content for API < 16 devices. - .setContentText(stringProvider.getString(StringR.string.notification_new_messages)) + .setContentText(stringProvider.getString(R.string.notification_new_messages)) // Number of new notifications for API <24 (M and below) devices. .setSubText( stringProvider.getQuantityString( - StringR.plurals.room_new_messages_notification, + R.plurals.room_new_messages_notification, messageStyle.messages.size, messageStyle.messages.size ) @@ -292,7 +292,7 @@ class NotificationUtils @Inject constructor( NotificationCompat.Action.Builder( R.drawable.ic_material_done_all_white, - stringProvider.getString(StringR.string.action_mark_room_read), markRoomReadPendingIntent + stringProvider.getString(R.string.action_mark_room_read), markRoomReadPendingIntent ) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) .setShowsUserInterface(false) @@ -373,7 +373,7 @@ class NotificationUtils @Inject constructor( addAction( R.drawable.vector_notification_reject_invitation, - stringProvider.getString(StringR.string.action_reject), + stringProvider.getString(R.string.action_reject), rejectIntentPendingIntent ) @@ -390,7 +390,7 @@ class NotificationUtils @Inject constructor( ) addAction( R.drawable.vector_notification_accept_invitation, - stringProvider.getString(StringR.string.action_join), + stringProvider.getString(R.string.action_join), joinIntentPendingIntent ) @@ -693,7 +693,7 @@ class NotificationUtils @Inject constructor( 888, NotificationCompat.Builder(context, NOISY_NOTIFICATION_CHANNEL_ID) .setContentTitle(buildMeta.applicationName) - .setContentText(stringProvider.getString(StringR.string.settings_troubleshoot_test_push_notification_content)) + .setContentText(stringProvider.getString(R.string.settings_troubleshoot_test_push_notification_content)) .setSmallIcon(R.drawable.ic_notification) .setLargeIcon(getBitmap(context, R.drawable.element_logo_green)) .setColor(ContextCompat.getColor(context, R.color.notification_accent_color)) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt index 2a2311e5917..360c5c1bb55 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -19,13 +19,13 @@ package io.element.android.libraries.push.impl.notifications import android.graphics.Bitmap import androidx.core.app.NotificationCompat import androidx.core.app.Person +import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.services.toolbox.api.strings.StringProvider import me.gujun.android.span.Span import me.gujun.android.span.span import timber.log.Timber import javax.inject.Inject -import io.element.android.libraries.ui.strings.R as StringR class RoomGroupMessageCreator @Inject constructor( private val bitmapLoader: NotificationBitmapLoader, @@ -50,9 +50,9 @@ class RoomGroupMessageCreator @Inject constructor( } val tickerText = if (roomIsGroup) { - stringProvider.getString(StringR.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description) + stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description) } else { - stringProvider.getString(StringR.string.notification_ticker_text_dm, events.last().senderName, events.last().description) + stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description) } val largeBitmap = getRoomBitmap(events) @@ -99,7 +99,7 @@ class RoomGroupMessageCreator @Inject constructor( } when { event.isSmartReplyError() -> addMessage( - stringProvider.getString(StringR.string.notification_inline_reply_failed), + stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson ) @@ -121,7 +121,7 @@ class RoomGroupMessageCreator @Inject constructor( 1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDirect) else -> { stringProvider.getQuantityString( - StringR.plurals.notification_compat_summary_line_for_room, + R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt index 13598f343ad..86b741bbd78 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt @@ -18,11 +18,10 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification import androidx.core.app.NotificationCompat +import io.element.android.libraries.push.impl.R import io.element.android.services.toolbox.api.strings.StringProvider import javax.inject.Inject -import io.element.android.libraries.ui.strings.R as StringR - /** * ======== Build summary notification ========= * On Android 7.0 (API level 24) and higher, the system automatically builds a summary for @@ -66,10 +65,10 @@ class SummaryGroupMessageCreator @Inject constructor( // FIXME roomIdToEventMap.size is not correct, this is the number of rooms val nbEvents = roomNotifications.size + simpleNotifications.size - val sumTitle = stringProvider.getQuantityString(StringR.plurals.notification_compat_summary_title, nbEvents, nbEvents) + val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) summaryInboxStyle.setBigContentTitle(sumTitle) // TODO get latest event? - .setSummaryText(stringProvider.getQuantityString(StringR.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) + .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) return if (useCompleteNotificationFormat) { notificationUtils.buildSummaryListNotification( summaryInboxStyle, @@ -101,21 +100,21 @@ class SummaryGroupMessageCreator @Inject constructor( val messageNotificationCount = messageEventsCount + simpleEventsCount val privacyTitle = if (invitationEventsCount > 0) { - val invitationsStr = stringProvider.getQuantityString(StringR.plurals.notification_invitations, invitationEventsCount, invitationEventsCount) + val invitationsStr = stringProvider.getQuantityString(R.plurals.notification_invitations, invitationEventsCount, invitationEventsCount) if (messageNotificationCount > 0) { // Invitation and message val messageStr = stringProvider.getQuantityString( - StringR.plurals.room_new_messages_notification, + R.plurals.room_new_messages_notification, messageNotificationCount, messageNotificationCount ) if (roomCount > 1) { // In several rooms val roomStr = stringProvider.getQuantityString( - StringR.plurals.notification_unread_notified_messages_in_room_rooms, + R.plurals.notification_unread_notified_messages_in_room_rooms, roomCount, roomCount ) stringProvider.getString( - StringR.string.notification_unread_notified_messages_in_room_and_invitation, + R.string.notification_unread_notified_messages_in_room_and_invitation, messageStr, roomStr, invitationsStr @@ -123,7 +122,7 @@ class SummaryGroupMessageCreator @Inject constructor( } else { // In one room stringProvider.getString( - StringR.string.notification_unread_notified_messages_and_invitation, + R.string.notification_unread_notified_messages_and_invitation, messageStr, invitationsStr ) @@ -135,13 +134,13 @@ class SummaryGroupMessageCreator @Inject constructor( } else { // No invitation, only messages val messageStr = stringProvider.getQuantityString( - StringR.plurals.room_new_messages_notification, + R.plurals.room_new_messages_notification, messageNotificationCount, messageNotificationCount ) if (roomCount > 1) { // In several rooms - val roomStr = stringProvider.getQuantityString(StringR.plurals.notification_unread_notified_messages_in_room_rooms, roomCount, roomCount) - stringProvider.getString(StringR.string.notification_unread_notified_messages_in_room, messageStr, roomStr) + val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, roomCount, roomCount) + stringProvider.getString(R.string.notification_unread_notified_messages_in_room, messageStr, roomStr) } else { // In one room messageStr diff --git a/libraries/push/impl/src/main/res/values/temporary.xml b/libraries/push/impl/src/main/res/values/temporary.xml new file mode 100644 index 00000000000..b560669f571 --- /dev/null +++ b/libraries/push/impl/src/main/res/values/temporary.xml @@ -0,0 +1,62 @@ + + + + No valid Google Play Services APK found. Notifications may not work properly. + Choose how to receive notifications + Google Services + Background synchronization + + Listening for events + Noisy notifications + Silent notifications + Call + New Messages + Mark as read + Join + Reject + You are viewing the notification! Click me! + %1$s: %2$s + %1$s: %2$s %3$s + ** Failed to send - please open room + %1$s in %2$s and %3$s" + %1$s and %2$s" + %1$s in %2$s" + + %d new message + %d new messages + + + %d unread notified message + %d unread notified messages + + + %d room + %d rooms + + + %d invitation + %d invitations + + + %1$s: %2$d message + %1$s: %2$d messages + + + %d notification + %d notifications + + From 5ef97fe85be7fef3b2f403ac1127d4749613f457 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Mar 2023 12:03:17 +0200 Subject: [PATCH 051/104] Implement Push client secret store and test it. --- .../libraries/push/impl/VectorPushHandler.kt | 5 +- .../impl/clientsecret/PushClientSecret.kt | 35 +++++++++ .../clientsecret/PushClientSecretFactory.kt | 21 ++++++ .../PushClientSecretFactoryImpl.kt | 28 +++++++ .../impl/clientsecret/PushClientSecretImpl.kt | 45 +++++++++++ .../clientsecret/PushClientSecretStore.kt | 24 ++++++ .../PushClientSecretStoreDataStore.kt | 62 +++++++++++++++ .../FakePushClientSecretFactory.kt | 29 +++++++ .../InMemoryPushClientSecretStore.kt | 39 ++++++++++ .../clientsecret/PushClientSecretImplTest.kt | 75 +++++++++++++++++++ 10 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecret.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactory.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImpl.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStore.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStoreDataStore.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/FakePushClientSecretFactory.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/InMemoryPushClientSecretStore.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImplTest.kt diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt index 6a73cedf928..3e0d0a3d6d3 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt @@ -28,6 +28,7 @@ import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.push.api.store.PushDataStore +import io.element.android.libraries.push.impl.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.model.PushData import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver import io.element.android.libraries.push.impl.notifications.NotificationActionIds @@ -49,6 +50,7 @@ class VectorPushHandler @Inject constructor( // private val activeSessionHolder: ActiveSessionHolder, private val pushDataStore: PushDataStore, private val defaultPushDataStore: DefaultPushDataStore, + private val pushClientSecret: PushClientSecret, private val actionIds: NotificationActionIds, @ApplicationContext private val context: Context, private val buildMeta: BuildMeta @@ -114,7 +116,8 @@ class VectorPushHandler @Inject constructor( } /* TODO EAx - - Open session + - Retrieve secret and use pushClientSecret + - Open matching session - get the event - display the notif diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecret.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecret.kt new file mode 100644 index 00000000000..0db59e42f78 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecret.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.clientsecret + +interface PushClientSecret { + /** + * To call when registering a pusher. It will return the existing secret or create a new one. + */ + suspend fun getSecretForUser(userId: String): String + + /** + * To call when receiving a push containing a client secret. + * Return null if not found. + */ + suspend fun getUserIdFromSecret(clientSecret: String): String? + + /** + * To call when the user signs out. + */ + suspend fun resetSecretForUser(userId: String) +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactory.kt new file mode 100644 index 00000000000..4ab6c775e32 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactory.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.clientsecret + +interface PushClientSecretFactory { + fun create(): String +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt new file mode 100644 index 00000000000..8a23409558d --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.clientsecret + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import java.util.UUID + +@ContributesBinding(AppScope::class) +class PushClientSecretFactoryImpl : PushClientSecretFactory { + override fun create(): String { + return UUID.randomUUID().toString() + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImpl.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImpl.kt new file mode 100644 index 00000000000..96f4ee25fde --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImpl.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.clientsecret + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class PushClientSecretImpl @Inject constructor( + private val pushClientSecretFactory: PushClientSecretFactory, + private val pushClientSecretStore: PushClientSecretStore, +) : PushClientSecret { + override suspend fun getSecretForUser(userId: String): String { + val existingSecret = pushClientSecretStore.getSecret(userId) + if (existingSecret != null) { + return existingSecret + } + val newSecret = pushClientSecretFactory.create() + pushClientSecretStore.storeSecret(userId, newSecret) + return newSecret + } + + override suspend fun getUserIdFromSecret(clientSecret: String): String? { + return pushClientSecretStore.getUserIdFromSecret(clientSecret) + } + + override suspend fun resetSecretForUser(userId: String) { + pushClientSecretStore.resetSecret(userId) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStore.kt new file mode 100644 index 00000000000..f283ab46070 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStore.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.clientsecret + +interface PushClientSecretStore { + suspend fun storeSecret(userId: String, clientSecret: String) + suspend fun getSecret(userId: String): String? + suspend fun resetSecret(userId: String) + suspend fun getUserIdFromSecret(clientSecret: String): String? +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStoreDataStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStoreDataStore.kt new file mode 100644 index 00000000000..b3befee36bb --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStoreDataStore.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.clientsecret + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import kotlinx.coroutines.flow.first +import javax.inject.Inject + +private val Context.dataStore: DataStore by preferencesDataStore(name = "push_client_secret_store") + +@ContributesBinding(AppScope::class) +class PushClientSecretStoreDataStore @Inject constructor( + @ApplicationContext private val context: Context, +) : PushClientSecretStore { + override suspend fun storeSecret(userId: String, clientSecret: String) { + context.dataStore.edit { settings -> + settings[getPreferenceKeyForUser(userId)] = clientSecret + } + } + + override suspend fun getSecret(userId: String): String? { + return context.dataStore.data.first()[getPreferenceKeyForUser(userId)] + } + + override suspend fun resetSecret(userId: String) { + context.dataStore.edit { settings -> + settings.remove(getPreferenceKeyForUser(userId)) + } + } + + override suspend fun getUserIdFromSecret(clientSecret: String): String? { + val keyValues = context.dataStore.data.first().asMap() + val matchingKey = keyValues.keys.firstOrNull { + keyValues[it] == clientSecret + } + return matchingKey?.name + } + + private fun getPreferenceKeyForUser(userId: String) = stringPreferencesKey(userId) +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/FakePushClientSecretFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/FakePushClientSecretFactory.kt new file mode 100644 index 00000000000..25823a57e83 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/FakePushClientSecretFactory.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.clientsecret + +private const val A_SECRET_PREFIX = "A_SECRET_" + +class FakePushClientSecretFactory : PushClientSecretFactory { + private var index = 0 + + override fun create() = getSecretForUser(index++) + + fun getSecretForUser(i: Int): String { + return A_SECRET_PREFIX + i + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/InMemoryPushClientSecretStore.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/InMemoryPushClientSecretStore.kt new file mode 100644 index 00000000000..0bc826398a6 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/InMemoryPushClientSecretStore.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.clientsecret + +class InMemoryPushClientSecretStore : PushClientSecretStore { + private val secrets = mutableMapOf() + + fun getSecrets(): Map = secrets + + override suspend fun storeSecret(userId: String, clientSecret: String) { + secrets[userId] = clientSecret + } + + override suspend fun getSecret(userId: String): String? { + return secrets[userId] + } + + override suspend fun resetSecret(userId: String) { + secrets.remove(userId) + } + + override suspend fun getUserIdFromSecret(clientSecret: String): String? { + return secrets.keys.firstOrNull { secrets[it] == clientSecret } + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImplTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImplTest.kt new file mode 100644 index 00000000000..1a6d52e660a --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImplTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.libraries.push.impl.clientsecret + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private const val A_USER_ID_0 = "A_USER_ID_0" +private const val A_USER_ID_1 = "A_USER_ID_1" + +private const val A_UNKNOWN_SECRET = "A_UNKNOWN_SECRET" + +internal class PushClientSecretImplTest { + + @Test + fun test() = runTest { + val factory = FakePushClientSecretFactory() + val store = InMemoryPushClientSecretStore() + val sut = PushClientSecretImpl(factory, store) + + val secret0 = factory.getSecretForUser(0) + val secret1 = factory.getSecretForUser(1) + val secret2 = factory.getSecretForUser(2) + + assertThat(store.getSecrets()).isEmpty() + assertThat(sut.getUserIdFromSecret(secret0)).isNull() + // Create a secret + assertThat(sut.getSecretForUser(A_USER_ID_0)).isEqualTo(secret0) + assertThat(store.getSecrets()).hasSize(1) + // Same secret returned + assertThat(sut.getSecretForUser(A_USER_ID_0)).isEqualTo(secret0) + assertThat(store.getSecrets()).hasSize(1) + // Another secret returned for another user + assertThat(sut.getSecretForUser(A_USER_ID_1)).isEqualTo(secret1) + assertThat(store.getSecrets()).hasSize(2) + + // Get users from secrets + assertThat(sut.getUserIdFromSecret(secret0)).isEqualTo(A_USER_ID_0) + assertThat(sut.getUserIdFromSecret(secret1)).isEqualTo(A_USER_ID_1) + // Unknown secret + assertThat(sut.getUserIdFromSecret(A_UNKNOWN_SECRET)).isNull() + + // User signs out + sut.resetSecretForUser(A_USER_ID_0) + assertThat(store.getSecrets()).hasSize(1) + // Create a new secret after reset + assertThat(sut.getSecretForUser(A_USER_ID_0)).isEqualTo(secret2) + + // Check the store content + assertThat(store.getSecrets()).isEqualTo( + mapOf( + A_USER_ID_0 to secret2, + A_USER_ID_1 to secret1, + ) + ) + } +} From 3c62df7831601f0829a4abdd4dce73202ef50868 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Mar 2023 15:25:07 +0200 Subject: [PATCH 052/104] Use correct type (it's a type alias) --- .../matrix/impl/auth/RustMatrixAuthenticationService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index 13ebf1c0e8b..aff83c3b5aa 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -59,7 +59,7 @@ class RustMatrixAuthenticationService @Inject constructor( } override suspend fun getLatestSessionId(): SessionId? = withContext(coroutineDispatchers.io) { - sessionStore.getLatestSession()?.userId?.let { UserId(it) } + sessionStore.getLatestSession()?.userId?.let { SessionId(it) } } override suspend fun restoreSession(sessionId: SessionId): Result = withContext(coroutineDispatchers.io) { From 17b6a78c64ecfae8110fd3387a962d1f79fb4590 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Mar 2023 15:40:34 +0200 Subject: [PATCH 053/104] Add a db query to get all the Sessions. --- .../android/libraries/sessionstorage/api/SessionStore.kt | 1 + .../sessionstorage/impl/memory/InMemorySessionStore.kt | 4 ++++ .../libraries/sessionstorage/impl/DatabaseSessionStore.kt | 6 ++++++ .../element/android/libraries/matrix/session/SessionData.sq | 3 +++ .../sessionstorage/impl/DatabaseSessionStoreTests.kt | 3 ++- 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt index de0ec2f7271..1637bd809fd 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt @@ -22,6 +22,7 @@ interface SessionStore { fun isLoggedIn(): Flow suspend fun storeData(sessionData: SessionData) suspend fun getSession(sessionId: String): SessionData? + suspend fun getAllSessions(): List suspend fun getLatestSession(): SessionData? suspend fun removeSession(sessionId: String) } diff --git a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt b/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt index b73ffdeb9ae..ce5b6e24f24 100644 --- a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt +++ b/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt @@ -38,6 +38,10 @@ class InMemorySessionStore : SessionStore { return sessionDataFlow.value.takeIf { it?.userId == sessionId } } + override suspend fun getAllSessions(): List { + return listOfNotNull(sessionDataFlow.value) + } + override suspend fun getLatestSession(): SessionData? { return sessionDataFlow.value } diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt index 6c32bcd1f33..15c30247125 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt @@ -53,6 +53,12 @@ class DatabaseSessionStore @Inject constructor( ?.toApiModel() } + override suspend fun getAllSessions(): List { + return database.sessionDataQueries.selectAll() + .executeAsList() + .map { it.toApiModel() } + } + override suspend fun removeSession(sessionId: String) { database.sessionDataQueries.removeSession(sessionId) } diff --git a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq index d8fb15338cb..ea8471a36ac 100644 --- a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq +++ b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq @@ -10,6 +10,9 @@ CREATE TABLE SessionData ( selectFirst: SELECT * FROM SessionData LIMIT 1; +selectAll: +SELECT * FROM SessionData; + selectByUserId: SELECT * FROM SessionData WHERE userId = ?; diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt index 885c04af784..0260604f6eb 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt @@ -57,6 +57,7 @@ class DatabaseSessionStoreTests { databaseSessionStore.storeData(aSessionData.toApiModel()) assertThat(database.sessionDataQueries.selectFirst().executeAsOneOrNull()).isEqualTo(aSessionData) + assertThat(database.sessionDataQueries.selectAll().executeAsList().size).isEqualTo(1) } @Test @@ -88,6 +89,7 @@ class DatabaseSessionStoreTests { val foundSession = databaseSessionStore.getSession(aSessionData.userId)?.toDbModel() assertThat(foundSession).isEqualTo(aSessionData) + assertThat(database.sessionDataQueries.selectAll().executeAsList().size).isEqualTo(2) } @Test @@ -107,5 +109,4 @@ class DatabaseSessionStoreTests { assertThat(database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOneOrNull()).isNull() } - } From 8692fa4628aac998d941ba3b822c3e2a5f89c5c2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Mar 2023 16:00:54 +0200 Subject: [PATCH 054/104] Register pusher - WIP --- .../libraries/matrix/api/MatrixClient.kt | 2 + .../matrix/api/pusher/PushersService.kt | 21 ++++++ .../matrix/api/pusher/SetHttpPusherData.kt | 28 ++++++++ .../libraries/matrix/impl/RustMatrixClient.kt | 7 +- .../matrix/impl/pushers/RustPushersService.kt | 49 +++++++++++++ .../libraries/push/impl/GoogleFcmHelper.kt | 5 +- .../libraries/push/impl/PushersManager.kt | 68 ++++++++++--------- .../impl/VectorFirebaseMessagingService.kt | 9 ++- .../VectorUnifiedPushMessagingReceiver.kt | 4 +- .../PushClientSecretFactoryImpl.kt | 3 +- 10 files changed, 160 insertions(+), 36 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/SetHttpPusherData.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 8a991771a50..9c0d0c17ace 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.api import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.media.MediaResolver +import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource @@ -33,6 +34,7 @@ interface MatrixClient : Closeable { fun stopSync() fun mediaResolver(): MediaResolver fun sessionVerificationService(): SessionVerificationService + fun pushersService(): PushersService suspend fun logout() suspend fun loadUserDisplayName(): Result suspend fun loadUserAvatarURLString(): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt new file mode 100644 index 00000000000..a868aeab85f --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.matrix.api.pusher + +interface PushersService { + fun setHttpPusher(setHttpPusherData: SetHttpPusherData) +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/SetHttpPusherData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/SetHttpPusherData.kt new file mode 100644 index 00000000000..43a90f5be2d --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/SetHttpPusherData.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.matrix.api.pusher + +data class SetHttpPusherData( + val pushKey: String, + val appId: String, + val url: String, + val appDisplayName: String, + val deviceDisplayName: String, + val profileTag: String?, + val lang: String, + val defaultPayload: String, +) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 2fe34bfa556..4acafa321e8 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -21,11 +21,13 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaResolver +import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.impl.media.RustMediaResolver -import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.impl.pushers.RustPushersService import io.element.android.libraries.matrix.impl.room.RustMatrixRoom import io.element.android.libraries.matrix.impl.room.RustRoomSummaryDataSource import io.element.android.libraries.matrix.impl.sync.SlidingSyncObserverProxy @@ -60,6 +62,7 @@ class RustMatrixClient constructor( override val sessionId: UserId = UserId(client.userId()) private val verificationService = RustSessionVerificationService() + private val pushersService = RustPushersService(client) private var slidingSyncUpdateJob: Job? = null private val clientDelegate = object : ClientDelegate { @@ -162,6 +165,8 @@ class RustMatrixClient constructor( override fun sessionVerificationService(): SessionVerificationService = verificationService + override fun pushersService(): PushersService = pushersService + override fun startSync() { if (isSyncing.compareAndSet(false, true)) { slidingSyncObserverToken = slidingSync.sync() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt new file mode 100644 index 00000000000..39dd5c1d11b --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.matrix.impl.pushers + +import io.element.android.libraries.matrix.api.pusher.PushersService +import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.HttpPusherData +import org.matrix.rustcomponents.sdk.PushFormat +import org.matrix.rustcomponents.sdk.PusherIdentifiers +import org.matrix.rustcomponents.sdk.PusherKind + +class RustPushersService( + private val client: Client, +) : PushersService { + override fun setHttpPusher(setHttpPusherData: SetHttpPusherData) { + client.setPusher( + identifiers = PusherIdentifiers( + pushkey = setHttpPusherData.pushKey, + appId = setHttpPusherData.appId + ), + kind = PusherKind.Http( + data = HttpPusherData( + url = setHttpPusherData.url, + format = PushFormat.EVENT_ID_ONLY, + defaultPayload = setHttpPusherData.defaultPayload + ) + ), + appDisplayName = setHttpPusherData.appDisplayName, + deviceDisplayName = setHttpPusherData.deviceDisplayName, + profileTag = setHttpPusherData.profileTag, + lang = setHttpPusherData.lang + ) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt index 16ce8d73ab8..b8dc1ad384d 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt @@ -26,6 +26,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.DefaultPreferences +import kotlinx.coroutines.runBlocking import timber.log.Timber import javax.inject.Inject @@ -58,7 +59,9 @@ class GoogleFcmHelper @Inject constructor( .addOnSuccessListener { token -> storeFcmToken(token) if (registerPusher) { - pushersManager.enqueueRegisterPusherWithFcmKey(token) + runBlocking {// TODO + pushersManager.enqueueRegisterPusherWithFcmKey(token) + } } } .addOnFailureListener { e -> diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index 5525b04d4cc..20cbd078e27 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -16,8 +16,13 @@ package io.element.android.libraries.push.impl +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData +import io.element.android.libraries.push.impl.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.config.PushConfig import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest +import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.services.toolbox.api.appname.AppNameProvider import javax.inject.Inject @@ -31,6 +36,9 @@ class PushersManager @Inject constructor( private val appNameProvider: AppNameProvider, // private val getDeviceInfoUseCase: GetDeviceInfoUseCase, private val pushGatewayNotifyRequest: PushGatewayNotifyRequest, + private val pushClientSecret: PushClientSecret, + private val sessionStore: SessionStore, + private val matrixAuthenticationService: MatrixAuthenticationService, ) { suspend fun testPush() { pushGatewayNotifyRequest.execute( @@ -43,47 +51,45 @@ class PushersManager @Inject constructor( ) } - fun enqueueRegisterPusherWithFcmKey(pushKey: String)/*: UUID*/ { + suspend fun enqueueRegisterPusherWithFcmKey(pushKey: String) { return enqueueRegisterPusher(pushKey, PushConfig.pusher_http_url) } - fun enqueueRegisterPusher( + suspend fun enqueueRegisterPusher( pushKey: String, gateway: String - ) /*: UUID*/ { - /* - val currentSession = activeSessionHolder.getActiveSession() - val pusher = createHttpPusher(pushKey, gateway) - return currentSession.pushersService().enqueueAddHttpPusher(pusher) - - */ - // TODO EAx - // TODO() - // Get all sessions - // Register pusher - // Close sessions + ) { + // Register the pusher for all the sessions + sessionStore.getAllSessions().forEach { sessionData -> + val client = matrixAuthenticationService.restoreSession(SessionId(sessionData.userId)).getOrNull() + client ?: return@forEach + client.pushersService().setHttpPusher(createHttpPusher(pushKey, gateway, sessionData.userId)) + // Close sessions? + } } - private fun createHttpPusher( + private suspend fun createHttpPusher( pushKey: String, - gateway: String - ): Any = TODO() - /* - HttpPusher( - pushkey = pushKey, - appId = PushConfig.pusher_app_id, - profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(activeSessionHolder.getActiveSession().myUserId.hashCode()), - lang = localeProvider.current().language, - appDisplayName = appNameProvider.getAppName(), - deviceDisplayName = getDeviceInfoUseCase.execute().displayName().orEmpty(), - url = gateway, - enabled = true, - deviceId = activeSessionHolder.getActiveSession().sessionParams.deviceId ?: "MOBILE", - append = false, - withEventIdOnly = true, - ) + gateway: String, + userId: String, + ): SetHttpPusherData = + SetHttpPusherData( + pushKey = pushKey, + appId = PushConfig.pusher_app_id, + profileTag = DEFAULT_PUSHER_FILE_TAG + "_" /* TODO + abs(activeSessionHolder.getActiveSession().myUserId.hashCode())*/, + lang = "en", // TODO localeProvider.current().language, + appDisplayName = appNameProvider.getAppName(), + deviceDisplayName = "MyDevice", // TODO getDeviceInfoUseCase.execute().displayName().orEmpty(), + url = gateway, + defaultPayload = createDefaultPayload(pushClientSecret.getSecretForUser(userId)) + ) + /** + * Ex: {"cs":"sfvsdv"} */ + private fun createDefaultPayload(secretForUser: String): String { + return "{\"cs\":\"$secretForUser\"}" + } suspend fun registerEmailForPush(email: String) { TODO() diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt index e9bccf7cddb..81afe726f21 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt @@ -24,6 +24,9 @@ import io.element.android.libraries.push.api.store.PushDataStore import io.element.android.libraries.push.impl.config.PushConfig import io.element.android.libraries.push.impl.di.FirebaseMessagingServiceBindings import io.element.android.libraries.push.impl.parser.PushParser +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -38,6 +41,8 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { @Inject lateinit var vectorPushHandler: VectorPushHandler @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + private val coroutineScope = CoroutineScope(SupervisorJob()) + override fun onCreate() { super.onCreate() applicationContext.bindings().inject(this) @@ -51,7 +56,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { // TODO EAx activeSessionHolder.hasActiveSession() && unifiedPushHelper.isEmbeddedDistributor() ) { - pushersManager.enqueueRegisterPusher(token, PushConfig.pusher_http_url) + coroutineScope.launch { + pushersManager.enqueueRegisterPusher(token, PushConfig.pusher_http_url) + } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorUnifiedPushMessagingReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorUnifiedPushMessagingReceiver.kt index 0fcdbafb698..49a63ba4aad 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorUnifiedPushMessagingReceiver.kt @@ -81,7 +81,9 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { coroutineScope.launch { unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) { unifiedPushHelper.getPushGateway()?.let { - pushersManager.enqueueRegisterPusher(endpoint, it) + coroutineScope.launch { + pushersManager.enqueueRegisterPusher(endpoint, it) + } } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt index 8a23409558d..1d7a1e6247f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt @@ -19,9 +19,10 @@ package io.element.android.libraries.push.impl.clientsecret import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import java.util.UUID +import javax.inject.Inject @ContributesBinding(AppScope::class) -class PushClientSecretFactoryImpl : PushClientSecretFactory { +class PushClientSecretFactoryImpl @Inject constructor() : PushClientSecretFactory { override fun create(): String { return UUID.randomUUID().toString() } From 97526faf9ae791e475a162b9bb804df3b4033a15 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Mar 2023 18:04:32 +0200 Subject: [PATCH 055/104] Retrieve notification - WIP --- .../android/appnav/LoggedInFlowNode.kt | 7 +++ .../libraries/matrix/api/MatrixClient.kt | 2 + .../api/notification/NotificationData.kt | 27 ++++++++++ .../api/notification/NotificationService.kt | 21 ++++++++ .../libraries/matrix/impl/RustMatrixClient.kt | 5 ++ .../impl/notification/NotificationMapper.kt | 51 +++++++++++++++++++ .../notification/RustNotificationService.kt | 39 ++++++++++++++ libraries/push/api/build.gradle.kts | 1 + .../android/libraries/push/api/PushService.kt | 5 ++ .../libraries/push/impl/DefaultPushService.kt | 5 ++ .../libraries/push/impl/PushersManager.kt | 11 ++++ .../libraries/push/impl/VectorPushHandler.kt | 44 +++++++++++++--- .../libraries/push/impl/model/PushData.kt | 3 +- .../libraries/push/impl/model/PushDataFcm.kt | 17 ++++--- .../push/impl/model/PushDataUnifiedPush.kt | 11 ++-- .../libraries/push/impl/parser/PushParser.kt | 1 + 16 files changed, 228 insertions(+), 22 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 1028c6f16dd..42d82913b82 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -52,9 +52,11 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.MAIN_SPACE import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.di.MatrixUIBindings +import io.element.android.libraries.push.api.PushService import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import kotlinx.parcelize.Parcelize import kotlin.coroutines.coroutineContext @@ -69,6 +71,7 @@ class LoggedInFlowNode @AssistedInject constructor( private val verifySessionEntryPoint: VerifySessionEntryPoint, private val coroutineScope: CoroutineScope, snackbarDispatcher: SnackbarDispatcher, + private val pushService: PushService, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.RoomList, @@ -111,6 +114,10 @@ class LoggedInFlowNode @AssistedInject constructor( // TODO We do not support Space yet, so directly navigate to main space appNavigationStateService.onNavigateToSpace(MAIN_SPACE) loggedInFlowProcessor.observeEvents(coroutineScope) + runBlocking { + // TODO + pushService.registerPusher(inputs.matrixClient.sessionId) + } }, onDestroy = { val imageLoaderFactory = bindings().notLoggedInImageLoaderFactory() diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 9c0d0c17ace..82c70747298 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.api import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.media.MediaResolver +import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -35,6 +36,7 @@ interface MatrixClient : Closeable { fun mediaResolver(): MediaResolver fun sessionVerificationService(): SessionVerificationService fun pushersService(): PushersService + fun notificationService(): NotificationService suspend fun logout() suspend fun loadUserDisplayName(): Result suspend fun loadUserAvatarURLString(): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt new file mode 100644 index 00000000000..27fc15c2c6f --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.matrix.api.notification + +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem + +data class NotificationData( + val item: MatrixTimelineItem, + val title: String, + val subtitle: String?, + val isNoisy: Boolean, + val avatarUrl: String?, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt new file mode 100644 index 00000000000..2c1672d8642 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.matrix.api.notification + +interface NotificationService { + fun getNotification(userId: String, roomId: String, eventId: String): NotificationData? +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 4acafa321e8..0fa5a668c8d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -21,12 +21,14 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaResolver +import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.impl.media.RustMediaResolver +import io.element.android.libraries.matrix.impl.notification.RustNotificationService import io.element.android.libraries.matrix.impl.pushers.RustPushersService import io.element.android.libraries.matrix.impl.room.RustMatrixRoom import io.element.android.libraries.matrix.impl.room.RustRoomSummaryDataSource @@ -63,6 +65,7 @@ class RustMatrixClient constructor( private val verificationService = RustSessionVerificationService() private val pushersService = RustPushersService(client) + private val notificationService = RustNotificationService(baseDirectory) private var slidingSyncUpdateJob: Job? = null private val clientDelegate = object : ClientDelegate { @@ -167,6 +170,8 @@ class RustMatrixClient constructor( override fun pushersService(): PushersService = pushersService + override fun notificationService(): NotificationService = notificationService + override fun startSync() { if (isSyncing.compareAndSet(false, true)) { slidingSyncObserverToken = slidingSync.sync() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt new file mode 100644 index 00000000000..ae47beb7000 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.matrix.impl.notification + +import io.element.android.libraries.matrix.api.notification.NotificationData +import io.element.android.libraries.matrix.impl.timeline.MatrixTimelineItemMapper +import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper +import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper +import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper +import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper +import org.matrix.rustcomponents.sdk.NotificationItem +import org.matrix.rustcomponents.sdk.use +import javax.inject.Inject + +class NotificationMapper @Inject constructor() { + // TODO Inject and remove duplicate? + private val timelineItemFactory = MatrixTimelineItemMapper( + virtualTimelineItemMapper = VirtualTimelineItemMapper(), + eventTimelineItemMapper = EventTimelineItemMapper( + contentMapper = TimelineEventContentMapper( + eventMessageMapper = EventMessageMapper() + ) + ) + ) + + fun map(notificationItem: NotificationItem): NotificationData { + return notificationItem.use { + NotificationData( + item = timelineItemFactory.map(it.item), + title = it.title, + subtitle = it.subtitle, + isNoisy = it.isNoisy, + avatarUrl = it.avatarUrl, + ) + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt new file mode 100644 index 00000000000..27091c17eef --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.matrix.impl.notification + +import io.element.android.libraries.matrix.api.notification.NotificationData +import io.element.android.libraries.matrix.api.notification.NotificationService +import java.io.File + +class RustNotificationService( + private val baseDirectory: File, +) : NotificationService { + private val notificationMapper: NotificationMapper = NotificationMapper() + + override fun getNotification(userId: String, roomId: String, eventId: String): NotificationData? { + return org.matrix.rustcomponents.sdk.NotificationService( + basePath = File(baseDirectory, "sessions").absolutePath, + userId = userId + ).use { + // TODO Not implemented yet, see https://github.com/matrix-org/matrix-rust-sdk/issues/1628 + it.getNotificationItem(roomId, eventId)?.let { notificationItem -> + notificationMapper.map(notificationItem) + } + } + } +} diff --git a/libraries/push/api/build.gradle.kts b/libraries/push/api/build.gradle.kts index 27a68273646..be1bbc13efe 100644 --- a/libraries/push/api/build.gradle.kts +++ b/libraries/push/api/build.gradle.kts @@ -25,4 +25,5 @@ android { dependencies { implementation(libs.androidx.corektx) implementation(libs.coroutines.core) + implementation(projects.libraries.matrix.api) } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt index 335ed9426c8..77adb869c5b 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt @@ -16,10 +16,15 @@ package io.element.android.libraries.push.api +import io.element.android.libraries.matrix.api.core.UserId + interface PushService { fun setCurrentRoom(roomId: String?) fun setCurrentThread(threadId: String?) fun notificationStyleChanged() + // Ensure pusher is registered + suspend fun registerPusher(userId: UserId) + suspend fun testPush() } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt index bf3a2523154..e9b7efcdee8 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager import javax.inject.Inject @@ -39,6 +40,10 @@ class DefaultPushService @Inject constructor( notificationDrawerManager.notificationStyleChanged() } + override suspend fun registerPusher(userId: UserId) { + pusherManager.registerPusher(userId) + } + override suspend fun testPush() { pusherManager.testPush() } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index 20cbd078e27..8a713585b6a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.push.impl.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.config.PushConfig @@ -39,6 +40,7 @@ class PushersManager @Inject constructor( private val pushClientSecret: PushClientSecret, private val sessionStore: SessionStore, private val matrixAuthenticationService: MatrixAuthenticationService, + private val fcmHelper: FcmHelper, ) { suspend fun testPush() { pushGatewayNotifyRequest.execute( @@ -55,6 +57,7 @@ class PushersManager @Inject constructor( return enqueueRegisterPusher(pushKey, PushConfig.pusher_http_url) } + // TODO Rename suspend fun enqueueRegisterPusher( pushKey: String, gateway: String @@ -68,6 +71,14 @@ class PushersManager @Inject constructor( } } + suspend fun registerPusher(userId: UserId) { + val pushKey = fcmHelper.getFcmToken() ?: return + // Register the pusher for the session + val client = matrixAuthenticationService.restoreSession(userId).getOrNull() ?: return + client.pushersService().setHttpPusher(createHttpPusher(pushKey, PushConfig.pusher_http_url, userId.value)) + // Close sessions? + } + private suspend fun createHttpPusher( pushKey: String, gateway: String, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt index 3e0d0a3d6d3..f6a4b5d83d0 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt @@ -27,6 +27,8 @@ import io.element.android.libraries.androidutils.network.WifiDetector import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.push.api.store.PushDataStore import io.element.android.libraries.push.impl.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.model.PushData @@ -34,11 +36,7 @@ import io.element.android.libraries.push.impl.notifications.NotifiableEventResol import io.element.android.libraries.push.impl.notifications.NotificationActionIds import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager import io.element.android.libraries.push.impl.store.DefaultPushDataStore -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* import timber.log.Timber import javax.inject.Inject @@ -53,7 +51,8 @@ class VectorPushHandler @Inject constructor( private val pushClientSecret: PushClientSecret, private val actionIds: NotificationActionIds, @ApplicationContext private val context: Context, - private val buildMeta: BuildMeta + private val buildMeta: BuildMeta, + private val matrixAuthenticationService: MatrixAuthenticationService, ) { private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -115,9 +114,38 @@ class VectorPushHandler @Inject constructor( Timber.tag(loggerTag.value).d("## handleInternal()") } + pushData.roomId ?: return + pushData.eventId ?: return + + val clientSecret = pushData.clientSecret + val userId = if (clientSecret == null) { + // Should not happen. In this case, restore default session + null + } else { + // Get userId from client secret + pushClientSecret.getUserIdFromSecret(clientSecret) + } ?: run { + matrixAuthenticationService.getLatestSessionId()?.value + } + + if (userId == null) { + Timber.w("Unable to get a session") + return + } + + // Restore session + val session = matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull() ?: return + // TODO EAx, no need for a session? + val notificationData = session.notificationService().getNotification( + userId = userId, + roomId = pushData.roomId, + eventId = pushData.eventId, + ) + + Timber.w("Notification: $notificationData") + // TODO Display notification + /* TODO EAx - - Retrieve secret and use pushClientSecret - - Open matching session - get the event - display the notif diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt index 75bed1027b1..06445d7ca68 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt @@ -27,6 +27,5 @@ data class PushData( val eventId: String?, val roomId: String?, val unread: Int?, - - // TODO EAx Client secret + val clientSecret: String?, ) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataFcm.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataFcm.kt index 0e37c14e125..fbde04cc36d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataFcm.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataFcm.kt @@ -25,19 +25,22 @@ import io.element.android.libraries.matrix.api.core.MatrixPatterns * "event_id":"$anEventId", * "room_id":"!aRoomId", * "unread":"1", - * "prio":"high" + * "prio":"high", + * "cs":"" * } * * . */ data class PushDataFcm( - val eventId: String?, - val roomId: String?, - var unread: Int?, + val eventId: String?, + val roomId: String?, + var unread: Int?, + val clientSecret: String? ) fun PushDataFcm.toPushData() = PushData( - eventId = eventId?.takeIf { MatrixPatterns.isEventId(it) }, - roomId = roomId?.takeIf { MatrixPatterns.isRoomId(it) }, - unread = unread + eventId = eventId?.takeIf { MatrixPatterns.isEventId(it) }, + roomId = roomId?.takeIf { MatrixPatterns.isRoomId(it) }, + unread = unread, + clientSecret = clientSecret, ) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataUnifiedPush.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataUnifiedPush.kt index c4227b3db2f..fc4ed55783d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataUnifiedPush.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataUnifiedPush.kt @@ -38,7 +38,7 @@ import kotlinx.serialization.Serializable */ @Serializable data class PushDataUnifiedPush( - val notification: PushDataUnifiedPushNotification? + val notification: PushDataUnifiedPushNotification? ) @Serializable @@ -50,11 +50,12 @@ data class PushDataUnifiedPushNotification( @Serializable data class PushDataUnifiedPushCounts( - @SerialName("unread") val unread: Int? + @SerialName("unread") val unread: Int? ) fun PushDataUnifiedPush.toPushData() = PushData( - eventId = notification?.eventId?.takeIf { MatrixPatterns.isEventId(it) }, - roomId = notification?.roomId?.takeIf { MatrixPatterns.isRoomId(it) }, - unread = notification?.counts?.unread + eventId = notification?.eventId?.takeIf { MatrixPatterns.isEventId(it) }, + roomId = notification?.roomId?.takeIf { MatrixPatterns.isRoomId(it) }, + unread = notification?.counts?.unread, + clientSecret = null // TODO EAx check how client secret will be sent through UnifiedPush ) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/parser/PushParser.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/parser/PushParser.kt index 7413264d5d2..1504e6ec00f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/parser/PushParser.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/parser/PushParser.kt @@ -50,6 +50,7 @@ class PushParser @Inject constructor() { eventId = message["event_id"], roomId = message["room_id"], unread = message["unread"]?.let { tryOrNull { Integer.parseInt(it) } }, + clientSecret = message["cs"], ) return pushDataFcm.toPushData() } From 8c5765cfcc204a1a0e3a157881932ac284355b75 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Mar 2023 14:18:23 +0200 Subject: [PATCH 056/104] Show basic notification when push is recieve --- .../android/x/intent/IntentProviderImpl.kt | 36 +++++++++++++++++++ .../libraries/push/impl/PushersManager.kt | 4 +-- .../libraries/push/impl/VectorPushHandler.kt | 3 ++ .../push/impl/intent/IntentProvider.kt | 26 ++++++++++++++ .../notifications/NotificationDisplayer.kt | 12 +++++-- .../NotificationDrawerManager.kt | 10 ++++-- .../impl/notifications/NotificationFactory.kt | 4 +++ .../notifications/NotificationRenderer.kt | 9 +++++ .../impl/notifications/NotificationUtils.kt | 33 ++++++++++++----- .../impl/src/main/res/values/temporary.xml | 1 + 10 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt diff --git a/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt b/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt new file mode 100644 index 00000000000..f7b96f3aad3 --- /dev/null +++ b/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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 io.element.android.x.intent + +import android.content.Context +import android.content.Intent +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.push.impl.intent.IntentProvider +import io.element.android.x.MainActivity +import javax.inject.Inject + +// TODO EAx change to deep-link. +@ContributesBinding(AppScope::class) +class IntentProviderImpl @Inject constructor( + @ApplicationContext private val context: Context, +) : IntentProvider { + override fun getMainIntent(): Intent { + return Intent(context, MainActivity::class.java) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index 8a713585b6a..b4952f35b4e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -67,7 +67,7 @@ class PushersManager @Inject constructor( val client = matrixAuthenticationService.restoreSession(SessionId(sessionData.userId)).getOrNull() client ?: return@forEach client.pushersService().setHttpPusher(createHttpPusher(pushKey, gateway, sessionData.userId)) - // Close sessions? + // TODO EAx Close sessions } } @@ -76,7 +76,7 @@ class PushersManager @Inject constructor( // Register the pusher for the session val client = matrixAuthenticationService.restoreSession(userId).getOrNull() ?: return client.pushersService().setHttpPusher(createHttpPusher(pushKey, PushConfig.pusher_http_url, userId.value)) - // Close sessions? + // TODO EAx Close sessions } private suspend fun createHttpPusher( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt index f6a4b5d83d0..0357e40a0af 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt @@ -142,9 +142,12 @@ class VectorPushHandler @Inject constructor( eventId = pushData.eventId, ) + // TODO Remove Timber.w("Notification: $notificationData") // TODO Display notification + notificationDrawerManager.displayTemporaryNotification() + /* TODO EAx - get the event - display the notif diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt new file mode 100644 index 00000000000..936ccfcbde7 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.intent + +import android.content.Intent + +interface IntentProvider { + /** + * Provide an intent to start the application + */ + fun getMainIntent(): Intent +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt index 7f9ec73343b..838356b370e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt @@ -16,20 +16,28 @@ package io.element.android.libraries.push.impl.notifications +import android.Manifest import android.app.Notification import android.content.Context +import android.content.pm.PackageManager +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationManagerCompat import io.element.android.libraries.di.ApplicationContext import timber.log.Timber import javax.inject.Inject +const val TEMPORARY_ID = 101 + class NotificationDisplayer @Inject constructor( - @ApplicationContext context: Context, + @ApplicationContext private val context: Context, ) { - private val notificationManager = NotificationManagerCompat.from(context) fun showNotificationMessage(tag: String?, id: Int, notification: Notification) { + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + Timber.w("Not allowed to notify.") + return + } notificationManager.notify(tag, id, notification) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt index c40680a1fd6..1bf3e68f7f4 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt @@ -41,7 +41,6 @@ import javax.inject.Inject @SingleIn(AppScope::class) class NotificationDrawerManager @Inject constructor( @ApplicationContext context: Context, - private val notificationDisplayer: NotificationDisplayer, private val pushDataStore: PushDataStore, // private val activeSessionDataSource: ActiveSessionDataSource, private val notifiableEventProcessor: NotifiableEventProcessor, @@ -154,7 +153,7 @@ class NotificationDrawerManager @Inject constructor( val newSettings = pushDataStore.useCompleteNotificationFormat() if (newSettings != useCompleteNotificationFormat) { // Settings has changed, remove all current notifications - notificationDisplayer.cancelAllNotifications() + notificationRenderer.cancelAllNotifications() useCompleteNotificationFormat = newSettings } } @@ -232,6 +231,13 @@ class NotificationDrawerManager @Inject constructor( return resolvedEvent.shouldIgnoreMessageEventInRoom(currentRoomId, currentThreadId) } + /** + * Temporary notification for EAx + */ + fun displayTemporaryNotification() { + notificationRenderer.displayTemporaryNotification() + } + companion object { const val SUMMARY_NOTIFICATION_ID = 0 const val ROOM_MESSAGES_NOTIFICATION_ID = 1 diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt index f935a363667..5a3f0089637 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt @@ -105,6 +105,10 @@ class NotificationFactory @Inject constructor( ) } } + + fun createTemporaryNotification(): Notification { + return notificationUtils.createTemporaryNotification() + } } sealed interface RoomNotification { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt index 8b5fa703654..e0fc44cca08 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt @@ -104,6 +104,15 @@ class NotificationRenderer @Inject constructor( } } } + + fun cancelAllNotifications() { + notificationDisplayer.cancelAllNotifications() + } + + fun displayTemporaryNotification() { + val notification = notificationFactory.createTemporaryNotification() + notificationDisplayer.showNotificationMessage(null, TEMPORARY_ID, notification) + } } private fun List>.groupByType(): GroupedNotificationEvents { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index bfa80908bf8..3fce6e0d84c 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -45,6 +45,7 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn import io.element.android.libraries.push.impl.R +import io.element.android.libraries.push.impl.intent.IntentProvider import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import io.element.android.services.toolbox.api.strings.StringProvider @@ -61,6 +62,7 @@ class NotificationUtils @Inject constructor( private val stringProvider: StringProvider, private val clock: SystemClock, private val actionIds: NotificationActionIds, + private val intentProvider: IntentProvider, private val buildMeta: BuildMeta, ) { @@ -107,6 +109,10 @@ class NotificationUtils @Inject constructor( private val notificationManager = NotificationManagerCompat.from(context) + init { + createNotificationChannels() + } + /* ========================================================================================== * Channel names * ========================================================================================== */ @@ -114,7 +120,7 @@ class NotificationUtils @Inject constructor( /** * Create notification channels. */ - fun createNotificationChannels() { + private fun createNotificationChannels() { if (!supportNotificationChannels()) { return } @@ -650,14 +656,6 @@ class NotificationUtils @Inject constructor( ) } - fun showNotificationMessage(tag: String?, id: Int, notification: Notification) { - notificationManager.notify(tag, id, notification) - } - - fun cancelNotificationMessage(tag: String?, id: Int) { - notificationManager.cancel(tag, id) - } - /** * Cancel the foreground notification service. */ @@ -705,6 +703,23 @@ class NotificationUtils @Inject constructor( ) } + fun createTemporaryNotification(): Notification { + val contentIntent = intentProvider.getMainIntent() + val pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, PendingIntentCompat.FLAG_IMMUTABLE) + + return NotificationCompat.Builder(context, NOISY_NOTIFICATION_CHANNEL_ID) + .setContentTitle(buildMeta.applicationName) + .setContentText(stringProvider.getString(R.string.notification_new_messages_temporary)) + .setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(getBitmap(context, R.drawable.element_logo_green)) + .setColor(ContextCompat.getColor(context, R.color.notification_accent_color)) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setCategory(NotificationCompat.CATEGORY_STATUS) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .build() + } + private fun getBitmap(context: Context, @DrawableRes drawableRes: Int): Bitmap? { val drawable = ResourcesCompat.getDrawable(context.resources, drawableRes, null) ?: return null val canvas = Canvas() diff --git a/libraries/push/impl/src/main/res/values/temporary.xml b/libraries/push/impl/src/main/res/values/temporary.xml index b560669f571..e7b8618cc26 100644 --- a/libraries/push/impl/src/main/res/values/temporary.xml +++ b/libraries/push/impl/src/main/res/values/temporary.xml @@ -25,6 +25,7 @@ Silent notifications Call New Messages + You have new message(s) Mark as read Join Reject From 1e6f2bc5a63836cc97c4a1bf0122d0ddb2120a37 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Mar 2023 14:47:49 +0200 Subject: [PATCH 057/104] Add missing Fake classes --- .../libraries/matrix/test/FakeMatrixClient.kt | 12 ++++++++- .../notification/FakeNotificationService.kt | 26 +++++++++++++++++++ .../matrix/test/pushers/FakePushersService.kt | 24 +++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 998c7cdea40..b44bbc9af62 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -20,11 +20,15 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.media.MediaResolver +import io.element.android.libraries.matrix.api.notification.NotificationService +import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.test.media.FakeMediaResolver +import io.element.android.libraries.matrix.test.notification.FakeNotificationService +import io.element.android.libraries.matrix.test.pushers.FakePushersService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService @@ -35,7 +39,9 @@ class FakeMatrixClient( private val userDisplayName: Result = Result.success(A_USER_NAME), private val userAvatarURLString: Result = Result.success(AN_AVATAR_URL), override val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(), - private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService() + private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), + private val pushersService: FakePushersService = FakePushersService(), + private val notificationService: FakeNotificationService = FakeNotificationService(), ) : MatrixClient { private var logoutFailure: Throwable? = null @@ -81,6 +87,10 @@ class FakeMatrixClient( override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService + override fun pushersService(): PushersService = pushersService + + override fun notificationService(): NotificationService = notificationService + override fun onSlidingSyncUpdate() {} override fun roomMembershipObserver(): RoomMembershipObserver { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt new file mode 100644 index 00000000000..a788e56f19e --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.matrix.test.notification + +import io.element.android.libraries.matrix.api.notification.NotificationData +import io.element.android.libraries.matrix.api.notification.NotificationService + +class FakeNotificationService : NotificationService { + override fun getNotification(userId: String, roomId: String, eventId: String): NotificationData? { + return null + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt new file mode 100644 index 00000000000..b9f4580ee83 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.matrix.test.pushers + +import io.element.android.libraries.matrix.api.pusher.PushersService +import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData + +class FakePushersService : PushersService { + override fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Unit +} From c64702c9f6d9b1cdb95986529a571c26d11381df Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Mar 2023 15:05:08 +0200 Subject: [PATCH 058/104] Create LoggedIn presenter --- appnav/build.gradle.kts | 4 + .../android/appnav/LoggedInFlowNode.kt | 26 ++++--- .../android/appnav/loggedin/LoggedInEvents.kt | 22 ++++++ .../appnav/loggedin/LoggedInPresenter.kt | 62 +++++++++++++++ .../android/appnav/loggedin/LoggedInState.kt | 24 ++++++ .../appnav/loggedin/LoggedInStateProvider.kt | 33 ++++++++ .../android/appnav/loggedin/LoggedInView.kt | 76 +++++++++++++++++++ features/roomlist/impl/build.gradle.kts | 2 - .../roomlist/impl/RoomListPresenter.kt | 17 ----- .../features/roomlist/impl/RoomListState.kt | 2 - .../roomlist/impl/RoomListStateProvider.kt | 2 - .../features/roomlist/impl/RoomListView.kt | 30 ++------ .../roomlist/impl/RoomListPresenterTests.kt | 8 -- .../android/samples/minimal/RoomListScreen.kt | 3 - 14 files changed, 243 insertions(+), 68 deletions(-) create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index b2d2b391d82..8ece3e5841e 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -41,12 +41,16 @@ dependencies { allFeaturesApi(rootDir) implementation(projects.libraries.core) + implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.push.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.matrixui) implementation(projects.libraries.uiStrings) + implementation(projects.libraries.permissions.api) + implementation(projects.libraries.permissions.noop) + implementation(projects.features.verifysession.api) implementation(projects.features.roomdetails.api) implementation(projects.tests.uitests) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 42d82913b82..b20dcc8f443 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -35,6 +35,8 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.appnav.loggedin.LoggedInPresenter +import io.element.android.appnav.loggedin.LoggedInView import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint @@ -52,7 +54,6 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.MAIN_SPACE import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.di.MatrixUIBindings -import io.element.android.libraries.push.api.PushService import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -71,7 +72,7 @@ class LoggedInFlowNode @AssistedInject constructor( private val verifySessionEntryPoint: VerifySessionEntryPoint, private val coroutineScope: CoroutineScope, snackbarDispatcher: SnackbarDispatcher, - private val pushService: PushService, + private val loggedInPresenter: LoggedInPresenter, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.RoomList, @@ -114,10 +115,6 @@ class LoggedInFlowNode @AssistedInject constructor( // TODO We do not support Space yet, so directly navigate to main space appNavigationStateService.onNavigateToSpace(MAIN_SPACE) loggedInFlowProcessor.observeEvents(coroutineScope) - runBlocking { - // TODO - pushService.registerPusher(inputs.matrixClient.sessionId) - } }, onDestroy = { val imageLoaderFactory = bindings().notLoggedInImageLoaderFactory() @@ -208,11 +205,16 @@ class LoggedInFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - Children( - navModel = backstack, - modifier = modifier, - // Animate navigation to settings and to a room - transitionHandler = rememberDefaultTransitionHandler(), - ) + val loggedInState = loggedInPresenter.present() + LoggedInView( + state = loggedInState + ) { + Children( + navModel = backstack, + modifier = modifier, + // Animate navigation to settings and to a room + transitionHandler = rememberDefaultTransitionHandler(), + ) + } } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt new file mode 100644 index 00000000000..2712b420033 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 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 io.element.android.appnav.loggedin + +// TODO Add your events or remove the file completely if no events +sealed interface LoggedInEvents { + object MyEvent : LoggedInEvents +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt new file mode 100644 index 00000000000..62127a5ea7f --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 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 io.element.android.appnav.loggedin + +import android.Manifest +import android.os.Build +import androidx.compose.runtime.Composable +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter +import io.element.android.libraries.push.api.PushService +import javax.inject.Inject + +class LoggedInPresenter @Inject constructor( + private val permissionsPresenterFactory: PermissionsPresenter.Factory, + // private val matrixClient: MatrixClient, + // private val pushService: PushService, +) : Presenter { + + private val postNotificationPermissionsPresenter by lazy { + // Ask for POST_NOTIFICATION PERMISSION on Android 13+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissionsPresenterFactory.create(Manifest.permission.POST_NOTIFICATIONS) + } else { + NoopPermissionsPresenter() + } + } + + @Composable + override fun present(): LoggedInState { + + // TODO EAx pushService.registerPusher(matrixClient.sessionId) + + val permissionsState = postNotificationPermissionsPresenter.present() + + fun handleEvents(event: LoggedInEvents) { + when (event) { + LoggedInEvents.MyEvent -> Unit + } + } + + return LoggedInState( + permissionsState = permissionsState, + eventSink = ::handleEvents + ) + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt new file mode 100644 index 00000000000..a5c43801bd9 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 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 io.element.android.appnav.loggedin + +import io.element.android.libraries.permissions.api.PermissionsState + +data class LoggedInState( + val permissionsState: PermissionsState, + val eventSink: (LoggedInEvents) -> Unit +) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt new file mode 100644 index 00000000000..90ff2136e57 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 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 io.element.android.appnav.loggedin + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState + +open class LoggedInStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aLoggedInState(), + // Add other state here + ) +} + +fun aLoggedInState() = LoggedInState( + permissionsState = createDummyPostNotificationPermissionsState(), + eventSink = {} +) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt new file mode 100644 index 00000000000..8071d7ca99f --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 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 io.element.android.appnav.loggedin + +import android.app.Activity +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.androidutils.system.openAppSettingsPage +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.permissions.api.PermissionsView + +@Composable +fun LoggedInView( + state: LoggedInState, + modifier: Modifier = Modifier, + children: @Composable BoxScope.() -> Unit, +) { + val activity = LocalContext.current as? Activity + + Box( + modifier = modifier + .fillMaxSize(), + contentAlignment = Alignment.TopCenter, + ) { + children() + + PermissionsView( + state = state.permissionsState, + openSystemSettings = { + activity?.let { openAppSettingsPage(it, "") } + } + ) + } +} + +@Preview +@Composable +fun LoggedInViewLightPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun LoggedInViewDarkPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: LoggedInState) { + LoggedInView( + state = state + ) { + Text("Children") + } +} diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index b679c2d8237..e5c0e289e0d 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -47,8 +47,6 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.elementresources) - implementation(projects.libraries.permissions.api) - implementation(projects.libraries.permissions.noop) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) implementation(projects.libraries.dateformatter.api) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index c38120161bc..ac37dfe30d3 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -16,8 +16,6 @@ package io.element.android.features.roomlist.impl -import android.Manifest -import android.os.Build import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -45,8 +43,6 @@ import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.ui.model.MatrixUser -import io.element.android.libraries.permissions.api.PermissionsPresenter -import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -63,20 +59,10 @@ class RoomListPresenter @Inject constructor( private val roomLastMessageFormatter: RoomLastMessageFormatter, private val sessionVerificationService: SessionVerificationService, private val snackbarDispatcher: SnackbarDispatcher, - private val permissionsPresenterFactory: PermissionsPresenter.Factory, ) : Presenter { private val roomMembershipObserver: RoomMembershipObserver = client.roomMembershipObserver() - private val postNotificationPermissionsPresenter by lazy { - // Ask for POST_NOTIFICATION PERMISSION on Android 13+ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - permissionsPresenterFactory.create(Manifest.permission.POST_NOTIFICATIONS) - } else { - NoopPermissionsPresenter() - } - } - @Composable override fun present(): RoomListState { val matrixUser: MutableState = remember { @@ -119,15 +105,12 @@ class RoomListPresenter @Inject constructor( val snackbarMessage = handleSnackbarMessage(snackbarDispatcher) - val permissionsState = postNotificationPermissionsPresenter.present() - return RoomListState( matrixUser = matrixUser.value, roomList = filteredRoomSummaries.value, filter = filter, displayVerificationPrompt = displayVerificationPrompt, snackbarMessage = snackbarMessage, - permissionsState = permissionsState, eventSink = ::handleEvents ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index 2deb13ff2bc..a14ef74e94e 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -20,7 +20,6 @@ import androidx.compose.runtime.Immutable import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.matrix.ui.model.MatrixUser -import io.element.android.libraries.permissions.api.PermissionsState import kotlinx.collections.immutable.ImmutableList @Immutable @@ -30,6 +29,5 @@ data class RoomListState( val filter: String, val displayVerificationPrompt: Boolean, val snackbarMessage: SnackbarMessage?, - val permissionsState: PermissionsState, val eventSink: (RoomListEvents) -> Unit ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 62fd918cca4..07e2fcff1a3 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -23,7 +23,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser -import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import io.element.android.libraries.ui.strings.R as StringR @@ -43,7 +42,6 @@ internal fun aRoomListState() = RoomListState( filter = "filter", snackbarMessage = null, displayVerificationPrompt = false, - permissionsState = createDummyPostNotificationPermissionsState(), eventSink = {} ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index 9567ef94648..b4c7676daf9 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -16,7 +16,6 @@ package io.element.android.features.roomlist.impl -import android.app.Activity import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -49,7 +48,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -59,7 +57,6 @@ import androidx.compose.ui.unit.dp import io.element.android.features.roomlist.impl.components.RoomListTopBar import io.element.android.features.roomlist.impl.components.RoomSummaryRow import io.element.android.features.roomlist.impl.model.RoomListRoomSummary -import io.element.android.libraries.androidutils.system.openAppSettingsPage import io.element.android.libraries.designsystem.ElementTextStyles import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -72,7 +69,6 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.coroutines.launch -import io.element.android.libraries.permissions.api.PermissionsView import io.element.android.libraries.designsystem.R as DrawableR import io.element.android.libraries.ui.strings.R as StringR @@ -85,24 +81,14 @@ fun RoomListView( onVerifyClicked: () -> Unit = {}, onCreateRoomClicked: () -> Unit = {}, ) { - val activity = LocalContext.current as? Activity - - Box(modifier = modifier) { - RoomListContent( - state = state, - modifier = Modifier, - onRoomClicked = onRoomClicked, - onOpenSettings = onOpenSettings, - onVerifyClicked = onVerifyClicked, - onCreateRoomClicked = onCreateRoomClicked, - ) - PermissionsView( - state = state.permissionsState, - openSystemSettings = { - activity?.let { openAppSettingsPage(it, "") } - } - ) - } + RoomListContent( + state = state, + modifier = modifier, + onRoomClicked = onRoomClicked, + onOpenSettings = onOpenSettings, + onVerifyClicked = onVerifyClicked, + onCreateRoomClicked = onCreateRoomClicked, + ) } @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index 18ead8f3e69..3f3e43e2e73 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -37,7 +37,6 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService -import io.element.android.libraries.permissions.noop.NoopPermissionsPresenterFactory import kotlinx.coroutines.test.runTest import org.junit.Test @@ -51,7 +50,6 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), - NoopPermissionsPresenterFactory(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -79,7 +77,6 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), - NoopPermissionsPresenterFactory(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -101,7 +98,6 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), - NoopPermissionsPresenterFactory(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -127,7 +123,6 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), - NoopPermissionsPresenterFactory(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -158,7 +153,6 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), - NoopPermissionsPresenterFactory(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -194,7 +188,6 @@ class RoomListPresenterTests { FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), SnackbarDispatcher(), - NoopPermissionsPresenterFactory(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -244,7 +237,6 @@ class RoomListPresenterTests { givenVerifiedStatus(SessionVerifiedStatus.NotVerified) }, SnackbarDispatcher(), - NoopPermissionsPresenterFactory(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 9d1c7a495d4..5dce2feafe3 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -29,7 +29,6 @@ import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.permissions.noop.NoopPermissionsPresenterFactory import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone @@ -45,14 +44,12 @@ class RoomListScreen( private val dateTimeProvider = LocalDateTimeProvider(clock, timeZone) private val dateFormatters = DateFormatters(locale, clock, timeZone) private val sessionVerificationService = matrixClient.sessionVerificationService() - private val permissionsPresenterFactory = NoopPermissionsPresenterFactory() private val presenter = RoomListPresenter( matrixClient, DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters), DefaultRoomLastMessageFormatter(context, matrixClient), sessionVerificationService, SnackbarDispatcher(), - permissionsPresenterFactory, ) @Composable From f5ca00ee6143f52a86edd8ccbcdad98e218f53a7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Mar 2023 10:15:51 +0200 Subject: [PATCH 059/104] Create a LoggedInNode, used as a PermanentNode in LoggedInFlowNode --- .../android/appnav/LoggedInFlowNode.kt | 19 ++++---- .../android/appnav/loggedin/LoggedInNode.kt | 44 +++++++++++++++++++ .../appnav/loggedin/LoggedInPresenter.kt | 11 +++-- .../android/appnav/loggedin/LoggedInView.kt | 33 ++++---------- .../android/libraries/push/api/PushService.kt | 8 +++- .../libraries/push/impl/DefaultPushService.kt | 6 +-- .../libraries/push/impl/PushersManager.kt | 10 ++--- 7 files changed, 85 insertions(+), 46 deletions(-) create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index b20dcc8f443..caef28bb9e2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -35,8 +35,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.appnav.loggedin.LoggedInPresenter -import io.element.android.appnav.loggedin.LoggedInView +import io.element.android.appnav.loggedin.LoggedInNode import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint @@ -72,7 +71,6 @@ class LoggedInFlowNode @AssistedInject constructor( private val verifySessionEntryPoint: VerifySessionEntryPoint, private val coroutineScope: CoroutineScope, snackbarDispatcher: SnackbarDispatcher, - private val loggedInPresenter: LoggedInPresenter, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.RoomList, @@ -128,6 +126,9 @@ class LoggedInFlowNode @AssistedInject constructor( } sealed interface NavTarget : Parcelable { + @Parcelize + object Permanent : NavTarget + @Parcelize object RoomList : NavTarget @@ -146,6 +147,9 @@ class LoggedInFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { + NavTarget.Permanent -> { + createNode(buildContext) + } NavTarget.RoomList -> { val callback = object : RoomListEntryPoint.Callback { override fun onRoomClicked(roomId: RoomId) { @@ -205,16 +209,15 @@ class LoggedInFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - val loggedInState = loggedInPresenter.present() - LoggedInView( - state = loggedInState - ) { + Box(modifier = modifier) { Children( navModel = backstack, - modifier = modifier, + modifier = Modifier, // Animate navigation to settings and to a room transitionHandler = rememberDefaultTransitionHandler(), ) + + PermanentChild(navTarget = NavTarget.Permanent) } } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt new file mode 100644 index 00000000000..6950b9b6998 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 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 io.element.android.appnav.loggedin + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class LoggedInNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val loggedInPresenter: LoggedInPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val loggedInState = loggedInPresenter.present() + LoggedInView( + state = loggedInState, + modifier = modifier + ) + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index 62127a5ea7f..a845ec4600c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -19,6 +19,7 @@ package io.element.android.appnav.loggedin import android.Manifest import android.os.Build import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.permissions.api.PermissionsPresenter @@ -27,9 +28,9 @@ import io.element.android.libraries.push.api.PushService import javax.inject.Inject class LoggedInPresenter @Inject constructor( + private val matrixClient: MatrixClient, private val permissionsPresenterFactory: PermissionsPresenter.Factory, - // private val matrixClient: MatrixClient, - // private val pushService: PushService, + private val pushService: PushService, ) : Presenter { private val postNotificationPermissionsPresenter by lazy { @@ -43,8 +44,10 @@ class LoggedInPresenter @Inject constructor( @Composable override fun present(): LoggedInState { - - // TODO EAx pushService.registerPusher(matrixClient.sessionId) + LaunchedEffect(Unit) { + // Ensure pusher is registered + pushService.registerPusher(matrixClient) + } val permissionsState = postNotificationPermissionsPresenter.present() diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt index 8071d7ca99f..5db19ccae7a 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt @@ -17,11 +17,7 @@ package io.element.android.appnav.loggedin import android.app.Activity -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview @@ -29,31 +25,22 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.libraries.androidutils.system.openAppSettingsPage import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.permissions.api.PermissionsView @Composable fun LoggedInView( state: LoggedInState, - modifier: Modifier = Modifier, - children: @Composable BoxScope.() -> Unit, + modifier: Modifier = Modifier ) { val activity = LocalContext.current as? Activity - Box( - modifier = modifier - .fillMaxSize(), - contentAlignment = Alignment.TopCenter, - ) { - children() - - PermissionsView( - state = state.permissionsState, - openSystemSettings = { - activity?.let { openAppSettingsPage(it, "") } - } - ) - } + PermissionsView( + state = state.permissionsState, + modifier = modifier, + openSystemSettings = { + activity?.let { openAppSettingsPage(it, "") } + } + ) } @Preview @@ -70,7 +57,5 @@ fun LoggedInViewDarkPreview(@PreviewParameter(LoggedInStateProvider::class) stat private fun ContentToPreview(state: LoggedInState) { LoggedInView( state = state - ) { - Text("Children") - } + ) } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt index 77adb869c5b..7d0f2cf4cb0 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt @@ -16,15 +16,19 @@ package io.element.android.libraries.push.api -import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.MatrixClient interface PushService { + // TODO EAx remove fun setCurrentRoom(roomId: String?) + + // TODO EAx remove fun setCurrentThread(threadId: String?) + fun notificationStyleChanged() // Ensure pusher is registered - suspend fun registerPusher(userId: UserId) + suspend fun registerPusher(matrixClient: MatrixClient) suspend fun testPush() } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt index e9b7efcdee8..82c7062959a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -18,7 +18,7 @@ package io.element.android.libraries.push.impl import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope -import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager import javax.inject.Inject @@ -40,8 +40,8 @@ class DefaultPushService @Inject constructor( notificationDrawerManager.notificationStyleChanged() } - override suspend fun registerPusher(userId: UserId) { - pusherManager.registerPusher(userId) + override suspend fun registerPusher(matrixClient: MatrixClient) { + pusherManager.registerPusher(matrixClient) } override suspend fun testPush() { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index b4952f35b4e..710688e5202 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -16,9 +16,9 @@ package io.element.android.libraries.push.impl +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.push.impl.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.config.PushConfig @@ -71,12 +71,12 @@ class PushersManager @Inject constructor( } } - suspend fun registerPusher(userId: UserId) { + suspend fun registerPusher(matrixClient: MatrixClient) { val pushKey = fcmHelper.getFcmToken() ?: return // Register the pusher for the session - val client = matrixAuthenticationService.restoreSession(userId).getOrNull() ?: return - client.pushersService().setHttpPusher(createHttpPusher(pushKey, PushConfig.pusher_http_url, userId.value)) - // TODO EAx Close sessions + matrixClient.pushersService().setHttpPusher( + createHttpPusher(pushKey, PushConfig.pusher_http_url, matrixClient.sessionId.value) + ) } private suspend fun createHttpPusher( From 657ed4a91c773d0613a750a60da1db70ba7451a4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Mar 2023 12:04:50 +0200 Subject: [PATCH 060/104] Add Result + Dispatcher on SDK call. --- .../matrix/api/pusher/PushersService.kt | 2 +- .../libraries/matrix/impl/RustMatrixClient.kt | 5 ++- .../matrix/impl/pushers/RustPushersService.kt | 41 +++++++++++-------- .../matrix/test/pushers/FakePushersService.kt | 2 +- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt index a868aeab85f..ef2291f8ce6 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt @@ -17,5 +17,5 @@ package io.element.android.libraries.matrix.api.pusher interface PushersService { - fun setHttpPusher(setHttpPusherData: SetHttpPusherData) + suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 0fa5a668c8d..bb4ec1c9716 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -64,7 +64,10 @@ class RustMatrixClient constructor( override val sessionId: UserId = UserId(client.userId()) private val verificationService = RustSessionVerificationService() - private val pushersService = RustPushersService(client) + private val pushersService = RustPushersService( + client = client, + dispatchers = dispatchers, + ) private val notificationService = RustNotificationService(baseDirectory) private var slidingSyncUpdateJob: Job? = null diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt index 39dd5c1d11b..4eaafef12d3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt @@ -16,8 +16,10 @@ package io.element.android.libraries.matrix.impl.pushers +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData +import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.HttpPusherData import org.matrix.rustcomponents.sdk.PushFormat @@ -26,24 +28,29 @@ import org.matrix.rustcomponents.sdk.PusherKind class RustPushersService( private val client: Client, + private val dispatchers: CoroutineDispatchers ) : PushersService { - override fun setHttpPusher(setHttpPusherData: SetHttpPusherData) { - client.setPusher( - identifiers = PusherIdentifiers( - pushkey = setHttpPusherData.pushKey, - appId = setHttpPusherData.appId - ), - kind = PusherKind.Http( - data = HttpPusherData( - url = setHttpPusherData.url, - format = PushFormat.EVENT_ID_ONLY, - defaultPayload = setHttpPusherData.defaultPayload + override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result { + return withContext(dispatchers.io) { + runCatching { + client.setPusher( + identifiers = PusherIdentifiers( + pushkey = setHttpPusherData.pushKey, + appId = setHttpPusherData.appId + ), + kind = PusherKind.Http( + data = HttpPusherData( + url = setHttpPusherData.url, + format = PushFormat.EVENT_ID_ONLY, + defaultPayload = setHttpPusherData.defaultPayload + ) + ), + appDisplayName = setHttpPusherData.appDisplayName, + deviceDisplayName = setHttpPusherData.deviceDisplayName, + profileTag = setHttpPusherData.profileTag, + lang = setHttpPusherData.lang ) - ), - appDisplayName = setHttpPusherData.appDisplayName, - deviceDisplayName = setHttpPusherData.deviceDisplayName, - profileTag = setHttpPusherData.profileTag, - lang = setHttpPusherData.lang - ) + } + } } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt index b9f4580ee83..77087d132f6 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt @@ -20,5 +20,5 @@ import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData class FakePushersService : PushersService { - override fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Unit + override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Result.success(Unit) } From 9d23d43026c9528bf9f9d447010e842ec4d2542a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Mar 2023 15:55:14 +0200 Subject: [PATCH 061/104] Cleanup + Add per user store. --- .../appnav/loggedin/LoggedInPresenter.kt | 2 +- .../android/libraries/push/api/PushService.kt | 2 +- .../push/impl/src/main/AndroidManifest.xml | 6 +- .../libraries/push/impl/AutoAcceptInvites.kt | 49 --------------- .../libraries/push/impl/DefaultPushService.kt | 15 +++-- .../libraries/push/impl/PushersManager.kt | 58 ++++++++++++----- .../libraries/push/impl/UnifiedPushHelper.kt | 1 - .../EnsureFcmTokenIsRetrievedUseCase.kt | 11 ++-- .../push/impl/firebase/FirebasePushParser.kt | 33 ++++++++++ .../PushDataFirebase.kt} | 9 +-- .../VectorFirebaseMessagingService.kt | 40 +++++------- ...VectorFirebaseMessagingServiceBindings.kt} | 5 +- .../libraries/push/impl/log/LoggerTag.kt | 21 +++++++ .../notifications/NotifiableEventProcessor.kt | 10 +-- .../libraries/push/impl/parser/PushParser.kt | 57 ----------------- .../push/impl/{model => push}/PushData.kt | 2 +- .../PushHandler.kt} | 17 +++-- .../{ => unifiedpush}/GuardServiceStarter.kt | 4 +- .../KeepInternalDistributor.kt | 2 +- .../PushDataUnifiedPush.kt | 5 +- .../RegisterUnifiedPushUseCase.kt | 4 +- .../impl/unifiedpush/UnifiedPushParser.kt | 29 +++++++++ .../UnregisterUnifiedPushUseCase.kt | 7 ++- .../VectorUnifiedPushMessagingReceiver.kt | 22 +++---- ...torUnifiedPushMessagingReceiverBindings.kt | 3 +- .../push/impl/userpushstore/UserPushStore.kt | 40 ++++++++++++ .../userpushstore/UserPushStoreDataStore.kt | 63 +++++++++++++++++++ .../userpushstore/UserPushStoreFactory.kt | 32 ++++++++++ .../sessionstorage/api/SessionStore.kt | 4 ++ 29 files changed, 351 insertions(+), 202 deletions(-) delete mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/AutoAcceptInvites.kt rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/{ => firebase}/EnsureFcmTokenIsRetrievedUseCase.kt (78%) create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/{model/PushDataFcm.kt => firebase/PushDataFirebase.kt} (83%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/{ => firebase}/VectorFirebaseMessagingService.kt (54%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/{di/FirebaseMessagingServiceBindings.kt => firebase/VectorFirebaseMessagingServiceBindings.kt} (82%) create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt delete mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/parser/PushParser.kt rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/{model => push}/PushData.kt (95%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/{VectorPushHandler.kt => push/PushHandler.kt} (94%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/{ => unifiedpush}/GuardServiceStarter.kt (90%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/{ => unifiedpush}/KeepInternalDistributor.kt (94%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/{model => unifiedpush}/PushDataUnifiedPush.kt (91%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/{ => unifiedpush}/RegisterUnifiedPushUseCase.kt (95%) create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParser.kt rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/{ => unifiedpush}/UnregisterUnifiedPushUseCase.kt (86%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/{ => unifiedpush}/VectorUnifiedPushMessagingReceiver.kt (88%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/{di => unifiedpush}/VectorUnifiedPushMessagingReceiverBindings.kt (86%) create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreDataStore.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index a845ec4600c..82f927a7430 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -46,7 +46,7 @@ class LoggedInPresenter @Inject constructor( override fun present(): LoggedInState { LaunchedEffect(Unit) { // Ensure pusher is registered - pushService.registerPusher(matrixClient) + pushService.registerFirebasePusher(matrixClient) } val permissionsState = postNotificationPermissionsPresenter.present() diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt index 7d0f2cf4cb0..5582e7fe921 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt @@ -28,7 +28,7 @@ interface PushService { fun notificationStyleChanged() // Ensure pusher is registered - suspend fun registerPusher(matrixClient: MatrixClient) + suspend fun registerFirebasePusher(matrixClient: MatrixClient) suspend fun testPush() } diff --git a/libraries/push/impl/src/main/AndroidManifest.xml b/libraries/push/impl/src/main/AndroidManifest.xml index 1d6f459d918..71fc629aaa2 100644 --- a/libraries/push/impl/src/main/AndroidManifest.xml +++ b/libraries/push/impl/src/main/AndroidManifest.xml @@ -26,7 +26,7 @@ android:value="true" /> @@ -35,7 +35,7 @@ @@ -48,7 +48,7 @@ diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/AutoAcceptInvites.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/AutoAcceptInvites.kt deleted file mode 100644 index cc2b9100ece..00000000000 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/AutoAcceptInvites.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2021 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.libraries.push.impl - -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import javax.inject.Inject - -// TODO Move away -/** - * This interface defines 2 flags so you can handle auto accept invites. - * At the moment we only have [CompileTimeAutoAcceptInvites] implementation. - */ -interface AutoAcceptInvites { - /** - * Enable auto-accept invites. It means, as soon as you got an invite from the sync, it will try to join it. - */ - val isEnabled: Boolean - - /** - * Hide invites from the UI (from notifications, notification count and room list). By default invites are hidden when [isEnabled] is true - */ - val hideInvites: Boolean - get() = isEnabled -} - -fun AutoAcceptInvites.showInvites() = !hideInvites - -/** - * Simple compile time implementation of AutoAcceptInvites flags. - */ -@ContributesBinding(AppScope::class) -class CompileTimeAutoAcceptInvites @Inject constructor() : AutoAcceptInvites { - override val isEnabled = false -} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt index 82c7062959a..327a3eea968 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -20,13 +20,17 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.api.PushService +import io.element.android.libraries.push.impl.config.PushConfig +import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager +import timber.log.Timber import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultPushService @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, - private val pusherManager: PushersManager, + private val pushersManager: PushersManager, + private val fcmHelper: FcmHelper, ) : PushService { override fun setCurrentRoom(roomId: String?) { notificationDrawerManager.setCurrentRoom(roomId) @@ -40,11 +44,14 @@ class DefaultPushService @Inject constructor( notificationDrawerManager.notificationStyleChanged() } - override suspend fun registerPusher(matrixClient: MatrixClient) { - pusherManager.registerPusher(matrixClient) + override suspend fun registerFirebasePusher(matrixClient: MatrixClient) { + val pushKey = fcmHelper.getFcmToken() ?: return Unit.also { + Timber.tag(pushLoggerTag.value).w("Unable to register pusher, Firebase token is not known.") + } + pushersManager.registerPusher(matrixClient, pushKey, PushConfig.pusher_http_url) } override suspend fun testPush() { - pusherManager.testPush() + pushersManager.testPush() } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index 710688e5202..84556245850 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -23,8 +23,12 @@ import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.push.impl.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.config.PushConfig import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest +import io.element.android.libraries.push.impl.userpushstore.UserPushStoreFactory +import io.element.android.libraries.push.impl.userpushstore.isFirebase import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.api.toUserList import io.element.android.services.toolbox.api.appname.AppNameProvider +import timber.log.Timber import javax.inject.Inject internal const val DEFAULT_PUSHER_FILE_TAG = "mobile" @@ -40,6 +44,7 @@ class PushersManager @Inject constructor( private val pushClientSecret: PushClientSecret, private val sessionStore: SessionStore, private val matrixAuthenticationService: MatrixAuthenticationService, + private val userPushStoreFactory: UserPushStoreFactory, private val fcmHelper: FcmHelper, ) { suspend fun testPush() { @@ -54,29 +59,54 @@ class PushersManager @Inject constructor( } suspend fun enqueueRegisterPusherWithFcmKey(pushKey: String) { - return enqueueRegisterPusher(pushKey, PushConfig.pusher_http_url) + // return onNewFirebaseToken(pushKey, PushConfig.pusher_http_url) + TODO() } - // TODO Rename - suspend fun enqueueRegisterPusher( + suspend fun onNewUnifiedPushEndpoint( pushKey: String, gateway: String ) { + TODO() + } + + suspend fun onNewFirebaseToken(firebaseToken: String) { + fcmHelper.storeFcmToken(firebaseToken) + // Register the pusher for all the sessions - sessionStore.getAllSessions().forEach { sessionData -> - val client = matrixAuthenticationService.restoreSession(SessionId(sessionData.userId)).getOrNull() - client ?: return@forEach - client.pushersService().setHttpPusher(createHttpPusher(pushKey, gateway, sessionData.userId)) - // TODO EAx Close sessions + sessionStore.getAllSessions().toUserList().forEach { userId -> + val userDataStore = userPushStoreFactory.create(userId) + if (userDataStore.isFirebase()) { + val client = matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull() + client ?: return@forEach + registerPusher(client, firebaseToken, PushConfig.pusher_http_url) + // TODO EAx Close sessions + } else { + Timber.d("This session is not using Firebase pusher") + } } } - suspend fun registerPusher(matrixClient: MatrixClient) { - val pushKey = fcmHelper.getFcmToken() ?: return - // Register the pusher for the session - matrixClient.pushersService().setHttpPusher( - createHttpPusher(pushKey, PushConfig.pusher_http_url, matrixClient.sessionId.value) - ) + /** + * Register a pusher to the server if not done yet. + */ + suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) { + val userDataStore = userPushStoreFactory.create(matrixClient.sessionId.value) + if (userDataStore.getCurrentRegisteredPushKey() == pushKey) { + Timber.d("Unnecessary to register again the same pusher") + } else { + // Register the pusher to the server + matrixClient.pushersService().setHttpPusher( + createHttpPusher(pushKey, gateway, matrixClient.sessionId.value) + ).fold( + { + userDataStore.setCurrentRegisteredPushKey(pushKey) + }, + { throwable -> + Timber.e(throwable, "Unable to register the pusher") + } + ) + } } private suspend fun createHttpPusher( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt index a6b50a58dd9..f77b5b28336 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt @@ -27,7 +27,6 @@ import org.unifiedpush.android.connector.UnifiedPush import timber.log.Timber import java.net.URL import javax.inject.Inject -import io.element.android.libraries.ui.strings.R as StringR class UnifiedPushHelper @Inject constructor( @ApplicationContext private val context: Context, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/EnsureFcmTokenIsRetrievedUseCase.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/EnsureFcmTokenIsRetrievedUseCase.kt similarity index 78% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/EnsureFcmTokenIsRetrievedUseCase.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/EnsureFcmTokenIsRetrievedUseCase.kt index fa5e6a0e5d7..9e9b28ecb8d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/EnsureFcmTokenIsRetrievedUseCase.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/EnsureFcmTokenIsRetrievedUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -14,13 +14,16 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl +package io.element.android.libraries.push.impl.firebase +import io.element.android.libraries.push.impl.FcmHelper +import io.element.android.libraries.push.impl.PushersManager +import io.element.android.libraries.push.impl.UnifiedPushHelper import javax.inject.Inject class EnsureFcmTokenIsRetrievedUseCase @Inject constructor( - private val unifiedPushHelper: UnifiedPushHelper, - private val fcmHelper: FcmHelper, + private val unifiedPushHelper: UnifiedPushHelper, + private val fcmHelper: FcmHelper, // private val activeSessionHolder: ActiveSessionHolder, ) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt new file mode 100644 index 00000000000..906816eb562 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.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 io.element.android.libraries.push.impl.firebase + +import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.push.impl.push.PushData +import javax.inject.Inject + +class FirebasePushParser @Inject constructor() { + fun parse(message: Map): PushData { + val pushDataFirebase = PushDataFirebase( + eventId = message["event_id"], + roomId = message["room_id"], + unread = message["unread"]?.let { tryOrNull { Integer.parseInt(it) } }, + clientSecret = message["cs"], + ) + return pushDataFirebase.toPushData() + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataFcm.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt similarity index 83% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataFcm.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt index fbde04cc36d..af82ebce74b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataFcm.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -14,9 +14,10 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.model +package io.element.android.libraries.push.impl.firebase import io.element.android.libraries.matrix.api.core.MatrixPatterns +import io.element.android.libraries.push.impl.push.PushData /** * In this case, the format is: @@ -31,14 +32,14 @@ import io.element.android.libraries.matrix.api.core.MatrixPatterns * * . */ -data class PushDataFcm( +data class PushDataFirebase( val eventId: String?, val roomId: String?, var unread: Int?, val clientSecret: String? ) -fun PushDataFcm.toPushData() = PushData( +fun PushDataFirebase.toPushData() = PushData( eventId = eventId?.takeIf { MatrixPatterns.isEventId(it) }, roomId = roomId?.takeIf { MatrixPatterns.isRoomId(it) }, unread = unread, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt similarity index 54% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt index 81afe726f21..f0e3bc16091 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorFirebaseMessagingService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt @@ -14,58 +14,50 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl +package io.element.android.libraries.push.impl.firebase import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag -import io.element.android.libraries.push.api.store.PushDataStore -import io.element.android.libraries.push.impl.config.PushConfig -import io.element.android.libraries.push.impl.di.FirebaseMessagingServiceBindings -import io.element.android.libraries.push.impl.parser.PushParser +import io.element.android.libraries.push.impl.PushersManager +import io.element.android.libraries.push.impl.push.PushHandler +import io.element.android.libraries.push.impl.log.pushLoggerTag import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("Push", LoggerTag.SYNC) +private val loggerTag = LoggerTag("Firebase", pushLoggerTag) class VectorFirebaseMessagingService : FirebaseMessagingService() { - @Inject lateinit var fcmHelper: FcmHelper - @Inject lateinit var pushDataStore: PushDataStore - // @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var pushersManager: PushersManager - @Inject lateinit var pushParser: PushParser - @Inject lateinit var vectorPushHandler: VectorPushHandler - @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + + @Inject + lateinit var pushParser: FirebasePushParser + + @Inject + lateinit var pushHandler: PushHandler private val coroutineScope = CoroutineScope(SupervisorJob()) override fun onCreate() { super.onCreate() - applicationContext.bindings().inject(this) + applicationContext.bindings().inject(this) } override fun onNewToken(token: String) { Timber.tag(loggerTag.value).d("New Firebase token") - fcmHelper.storeFcmToken(token) - if ( - // pushDataStore.areNotificationEnabledForDevice() && - // TODO EAx activeSessionHolder.hasActiveSession() && - unifiedPushHelper.isEmbeddedDistributor() - ) { - coroutineScope.launch { - pushersManager.enqueueRegisterPusher(token, PushConfig.pusher_http_url) - } + coroutineScope.launch { + pushersManager.onNewFirebaseToken(token) } } override fun onMessageReceived(message: RemoteMessage) { Timber.tag(loggerTag.value).d("New Firebase message") - pushParser.parsePushDataFcm(message.data).let { - vectorPushHandler.handle(it) + pushParser.parse(message.data).let { + pushHandler.handle(it) } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/FirebaseMessagingServiceBindings.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingServiceBindings.kt similarity index 82% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/FirebaseMessagingServiceBindings.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingServiceBindings.kt index 1de015b7706..aef87e7df32 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/FirebaseMessagingServiceBindings.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingServiceBindings.kt @@ -14,13 +14,12 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.di +package io.element.android.libraries.push.impl.firebase import com.squareup.anvil.annotations.ContributesTo import io.element.android.libraries.di.AppScope -import io.element.android.libraries.push.impl.VectorFirebaseMessagingService @ContributesTo(AppScope::class) -interface FirebaseMessagingServiceBindings { +interface VectorFirebaseMessagingServiceBindings { fun inject(service: VectorFirebaseMessagingService) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt new file mode 100644 index 00000000000..359779fd8ab --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.log + +import io.element.android.libraries.core.log.logger.LoggerTag + +internal val pushLoggerTag = LoggerTag("Push") diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt index 91b62eba0e0..209c42f4ae1 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt @@ -16,12 +16,7 @@ package io.element.android.libraries.push.impl.notifications -import io.element.android.libraries.push.impl.AutoAcceptInvites -import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent -import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent -import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent -import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent -import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreMessageEventInRoom +import io.element.android.libraries.push.impl.notifications.model.* import timber.log.Timber import javax.inject.Inject @@ -29,13 +24,12 @@ private typealias ProcessedEvents = List> class NotifiableEventProcessor @Inject constructor( private val outdatedDetector: OutdatedEventDetector, - private val autoAcceptInvites: AutoAcceptInvites ) { fun process(queuedEvents: List, currentRoomId: String?, currentThreadId: String?, renderedEvents: ProcessedEvents): ProcessedEvents { val processedEvents = queuedEvents.map { val type = when (it) { - is InviteNotifiableEvent -> if (autoAcceptInvites.hideInvites) ProcessedEvent.Type.REMOVE else ProcessedEvent.Type.KEEP + is InviteNotifiableEvent -> ProcessedEvent.Type.KEEP is NotifiableMessageEvent -> when { it.shouldIgnoreMessageEventInRoom(currentRoomId, currentThreadId) -> { ProcessedEvent.Type.REMOVE diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/parser/PushParser.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/parser/PushParser.kt deleted file mode 100644 index 1504e6ec00f..00000000000 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/parser/PushParser.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.libraries.push.impl.parser - -import io.element.android.libraries.core.data.tryOrNull -import io.element.android.libraries.push.impl.model.PushData -import io.element.android.libraries.push.impl.model.PushDataFcm -import io.element.android.libraries.push.impl.model.PushDataUnifiedPush -import io.element.android.libraries.push.impl.model.toPushData -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import javax.inject.Inject - -/** - * Parse the received data from Push. Json format are different depending on the source. - * - * Notifications received by FCM are formatted by the matrix gateway [1]. The data send to FCM is the content - * of the "notification" attribute of the json sent to the gateway [2][3]. - * On the other side, with UnifiedPush, the content of the message received is the content posted to the push - * gateway endpoint [3]. - * - * *Note*: If we want to get the same content with FCM and unifiedpush, we can do a new sygnal pusher [4]. - * - * [1] https://github.com/matrix-org/sygnal/blob/main/sygnal/gcmpushkin.py - * [2] https://github.com/matrix-org/sygnal/blob/main/sygnal/gcmpushkin.py#L366 - * [3] https://spec.matrix.org/latest/push-gateway-api/ - * [4] https://github.com/p1gp1g/sygnal/blob/unifiedpush/sygnal/upfcmpushkin.py (Not tested for a while) - */ -class PushParser @Inject constructor() { - fun parsePushDataUnifiedPush(message: ByteArray): PushData? { - return tryOrNull { Json.decodeFromString(String(message)) }?.toPushData() - } - - fun parsePushDataFcm(message: Map): PushData { - val pushDataFcm = PushDataFcm( - eventId = message["event_id"], - roomId = message["room_id"], - unread = message["unread"]?.let { tryOrNull { Integer.parseInt(it) } }, - clientSecret = message["cs"], - ) - return pushDataFcm.toPushData() - } -} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt similarity index 95% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt index 06445d7ca68..0955c864cdf 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushData.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.model +package io.element.android.libraries.push.impl.push /** * Represent parsed data that the app has received from a Push content. diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt similarity index 94% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt index 0357e40a0af..bab955b419b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl +package io.element.android.libraries.push.impl.push import android.content.Context import android.content.Intent @@ -30,19 +30,24 @@ import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.push.api.store.PushDataStore +import io.element.android.libraries.push.impl.PushersManager import io.element.android.libraries.push.impl.clientsecret.PushClientSecret -import io.element.android.libraries.push.impl.model.PushData import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver import io.element.android.libraries.push.impl.notifications.NotificationActionIds import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager +import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.store.DefaultPushDataStore -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("Push", LoggerTag.SYNC) +private val loggerTag = LoggerTag("Push", pushLoggerTag) -class VectorPushHandler @Inject constructor( +class PushHandler @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val notifiableEventResolver: NotifiableEventResolver, // private val activeSessionHolder: ActiveSessionHolder, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GuardServiceStarter.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/GuardServiceStarter.kt similarity index 90% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GuardServiceStarter.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/GuardServiceStarter.kt index 42993828a98..4c93b8a9291 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GuardServiceStarter.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/GuardServiceStarter.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright (c) 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl +package io.element.android.libraries.push.impl.unifiedpush import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/KeepInternalDistributor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/KeepInternalDistributor.kt similarity index 94% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/KeepInternalDistributor.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/KeepInternalDistributor.kt index d351067e524..de66ed3914b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/KeepInternalDistributor.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/KeepInternalDistributor.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl +package io.element.android.libraries.push.impl.unifiedpush import android.content.BroadcastReceiver import android.content.Context diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataUnifiedPush.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt similarity index 91% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataUnifiedPush.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt index fc4ed55783d..3e6a8199ecd 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/model/PushDataUnifiedPush.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -14,9 +14,10 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.model +package io.element.android.libraries.push.impl.unifiedpush import io.element.android.libraries.matrix.api.core.MatrixPatterns +import io.element.android.libraries.push.impl.push.PushData import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/RegisterUnifiedPushUseCase.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/RegisterUnifiedPushUseCase.kt similarity index 95% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/RegisterUnifiedPushUseCase.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/RegisterUnifiedPushUseCase.kt index e9f8cb985fc..50ca94f30db 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/RegisterUnifiedPushUseCase.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/RegisterUnifiedPushUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl +package io.element.android.libraries.push.impl.unifiedpush import android.content.Context import io.element.android.libraries.di.ApplicationContext diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParser.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParser.kt new file mode 100644 index 00000000000..9788ecf1a1d --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParser.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.unifiedpush + +import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.push.impl.push.PushData +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import javax.inject.Inject + +class UnifiedPushParser @Inject constructor() { + fun parse(message: ByteArray): PushData? { + return tryOrNull { Json.decodeFromString(String(message)) }?.toPushData() + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnregisterUnifiedPushUseCase.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnregisterUnifiedPushUseCase.kt similarity index 86% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnregisterUnifiedPushUseCase.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnregisterUnifiedPushUseCase.kt index 34c78a237e2..6cd1af1de36 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnregisterUnifiedPushUseCase.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnregisterUnifiedPushUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -14,12 +14,15 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl +package io.element.android.libraries.push.impl.unifiedpush import android.content.Context import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.push.api.model.BackgroundSyncMode import io.element.android.libraries.push.api.store.PushDataStore +import io.element.android.libraries.push.impl.PushersManager +import io.element.android.libraries.push.impl.UnifiedPushHelper +import io.element.android.libraries.push.impl.UnifiedPushStore import org.unifiedpush.android.connector.UnifiedPush import timber.log.Timber import javax.inject.Inject diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorUnifiedPushMessagingReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt similarity index 88% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorUnifiedPushMessagingReceiver.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index 49a63ba4aad..db4489a489a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -14,18 +14,18 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl +package io.element.android.libraries.push.impl.unifiedpush import android.content.Context import android.content.Intent import android.widget.Toast import io.element.android.libraries.architecture.bindings - import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.push.api.model.BackgroundSyncMode import io.element.android.libraries.push.api.store.PushDataStore -import io.element.android.libraries.push.impl.di.VectorUnifiedPushMessagingReceiverBindings -import io.element.android.libraries.push.impl.parser.PushParser +import io.element.android.libraries.push.impl.* +import io.element.android.libraries.push.impl.log.pushLoggerTag +import io.element.android.libraries.push.impl.push.PushHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -34,15 +34,15 @@ import org.unifiedpush.android.connector.MessagingReceiver import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("Push", LoggerTag.SYNC) +private val loggerTag = LoggerTag("Unified", pushLoggerTag) class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { @Inject lateinit var pushersManager: PushersManager - @Inject lateinit var pushParser: PushParser + @Inject lateinit var pushParser: UnifiedPushParser //@Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var pushDataStore: PushDataStore - @Inject lateinit var vectorPushHandler: VectorPushHandler + @Inject lateinit var pushHandler: PushHandler @Inject lateinit var guardServiceStarter: GuardServiceStarter @Inject lateinit var unifiedPushStore: UnifiedPushStore @Inject lateinit var unifiedPushHelper: UnifiedPushHelper @@ -64,8 +64,8 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { */ override fun onMessage(context: Context, message: ByteArray, instance: String) { Timber.tag(loggerTag.value).d("New message") - pushParser.parsePushDataUnifiedPush(message)?.let { - vectorPushHandler.handle(it) + pushParser.parse(message)?.let { + pushHandler.handle(it) } ?: run { Timber.tag(loggerTag.value).w("Invalid received data Json format") } @@ -82,7 +82,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) { unifiedPushHelper.getPushGateway()?.let { coroutineScope.launch { - pushersManager.enqueueRegisterPusher(endpoint, it) + pushersManager.onNewUnifiedPushEndpoint(endpoint, it) } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/VectorUnifiedPushMessagingReceiverBindings.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt similarity index 86% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/VectorUnifiedPushMessagingReceiverBindings.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt index 1a70d94ee4c..90857d990d1 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/VectorUnifiedPushMessagingReceiverBindings.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt @@ -14,11 +14,10 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.di +package io.element.android.libraries.push.impl.unifiedpush import com.squareup.anvil.annotations.ContributesTo import io.element.android.libraries.di.AppScope -import io.element.android.libraries.push.impl.VectorUnifiedPushMessagingReceiver @ContributesTo(AppScope::class) interface VectorUnifiedPushMessagingReceiverBindings { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt new file mode 100644 index 00000000000..a66b2835193 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.userpushstore + +const val NOTIFICATION_METHOD_FIREBASE = "NOTIFICATION_METHOD_FIREBASE" +const val NOTIFICATION_METHOD_UNIFIEDPUSH = "NOTIFICATION_METHOD_UNIFIEDPUSH" + +/** + * Store data related to push about a user. + */ +interface UserPushStore { + /** + * NOTIFICATION_METHOD_FIREBASE or NOTIFICATION_METHOD_UNIFIEDPUSH + */ + suspend fun getNotificationMethod(): String + + suspend fun setNotificationMethod(value: String) + + suspend fun getCurrentRegisteredPushKey(): String? + + suspend fun setCurrentRegisteredPushKey(value: String) + + suspend fun reset() +} + +suspend fun UserPushStore.isFirebase(): Boolean = getNotificationMethod() == NOTIFICATION_METHOD_FIREBASE diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreDataStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreDataStore.kt new file mode 100644 index 00000000000..6f25599e545 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreDataStore.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.userpushstore + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.first + +/** + * Store data related to push about a user. + */ +class UserPushStoreDataStore( + private val context: Context, + userId: String, +) : UserPushStore { + private val Context.dataStore: DataStore by preferencesDataStore(name = "push_store_$userId") + private val notificationMethod = stringPreferencesKey("notificationMethod") + private val currentPushKey = stringPreferencesKey("currentPushKey") + + override suspend fun getNotificationMethod(): String { + return context.dataStore.data.first()[notificationMethod] ?: NOTIFICATION_METHOD_FIREBASE + } + + override suspend fun setNotificationMethod(value: String) { + context.dataStore.edit { + it[notificationMethod] = value + } + } + + override suspend fun getCurrentRegisteredPushKey(): String? { + return context.dataStore.data.first()[currentPushKey] + } + + override suspend fun setCurrentRegisteredPushKey(value: String) { + context.dataStore.edit { + it[currentPushKey] = value + } + } + + override suspend fun reset() { + context.dataStore.edit { + it.clear() + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt new file mode 100644 index 00000000000..4b16a214915 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.userpushstore + +import android.content.Context +import io.element.android.libraries.di.ApplicationContext +import javax.inject.Inject + +class UserPushStoreFactory @Inject constructor( + @ApplicationContext private val context: Context, +) { + fun create(userId: String): UserPushStore { + return UserPushStoreDataStore( + context = context, + userId = userId + ) + } +} diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt index 1637bd809fd..223ab16aa55 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt @@ -26,3 +26,7 @@ interface SessionStore { suspend fun getLatestSession(): SessionData? suspend fun removeSession(sessionId: String) } + +fun List.toUserList(): List { + return map { it.userId } +} From 38b916e8c83448b2cbd9d153d88327b41e18d7a4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Mar 2023 15:59:23 +0200 Subject: [PATCH 062/104] Close MatrixClient after usage --- .../android/libraries/push/impl/PushersManager.kt | 7 +++---- .../android/libraries/push/impl/push/PushHandler.kt | 12 +++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index 84556245850..8407360385d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -77,10 +77,9 @@ class PushersManager @Inject constructor( sessionStore.getAllSessions().toUserList().forEach { userId -> val userDataStore = userPushStoreFactory.create(userId) if (userDataStore.isFirebase()) { - val client = matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull() - client ?: return@forEach - registerPusher(client, firebaseToken, PushConfig.pusher_http_url) - // TODO EAx Close sessions + matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client -> + registerPusher(client, firebaseToken, PushConfig.pusher_http_url) + } } else { Timber.d("This session is not using Firebase pusher") } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt index bab955b419b..2f7a1947c5f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt @@ -141,11 +141,13 @@ class PushHandler @Inject constructor( // Restore session val session = matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull() ?: return // TODO EAx, no need for a session? - val notificationData = session.notificationService().getNotification( - userId = userId, - roomId = pushData.roomId, - eventId = pushData.eventId, - ) + val notificationData = session.use { + it.notificationService().getNotification( + userId = userId, + roomId = pushData.roomId, + eventId = pushData.eventId, + ) + } // TODO Remove Timber.w("Notification: $notificationData") From 10418a0151942d56fb69a9377481248800b229da Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Mar 2023 16:02:05 +0200 Subject: [PATCH 063/104] Protect call to getNotificationItem --- .../api/notification/NotificationService.kt | 2 +- .../libraries/matrix/impl/RustMatrixClient.kt | 2 +- .../notification/RustNotificationService.kt | 23 ++++++++++++------- .../notification/FakeNotificationService.kt | 4 ++-- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt index 2c1672d8642..9dec0821a33 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt @@ -17,5 +17,5 @@ package io.element.android.libraries.matrix.api.notification interface NotificationService { - fun getNotification(userId: String, roomId: String, eventId: String): NotificationData? + suspend fun getNotification(userId: String, roomId: String, eventId: String): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index bb4ec1c9716..cd14b35fbce 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -68,7 +68,7 @@ class RustMatrixClient constructor( client = client, dispatchers = dispatchers, ) - private val notificationService = RustNotificationService(baseDirectory) + private val notificationService = RustNotificationService(baseDirectory, dispatchers) private var slidingSyncUpdateJob: Job? = null private val clientDelegate = object : ClientDelegate { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt index 27091c17eef..11970211611 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt @@ -16,23 +16,30 @@ package io.element.android.libraries.matrix.impl.notification +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationService +import kotlinx.coroutines.withContext import java.io.File class RustNotificationService( private val baseDirectory: File, + private val dispatchers: CoroutineDispatchers, ) : NotificationService { private val notificationMapper: NotificationMapper = NotificationMapper() - override fun getNotification(userId: String, roomId: String, eventId: String): NotificationData? { - return org.matrix.rustcomponents.sdk.NotificationService( - basePath = File(baseDirectory, "sessions").absolutePath, - userId = userId - ).use { - // TODO Not implemented yet, see https://github.com/matrix-org/matrix-rust-sdk/issues/1628 - it.getNotificationItem(roomId, eventId)?.let { notificationItem -> - notificationMapper.map(notificationItem) + override suspend fun getNotification(userId: String, roomId: String, eventId: String): Result { + return withContext(dispatchers.io) { + runCatching { + org.matrix.rustcomponents.sdk.NotificationService( + basePath = File(baseDirectory, "sessions").absolutePath, + userId = userId + ).use { + // TODO Not implemented yet, see https://github.com/matrix-org/matrix-rust-sdk/issues/1628 + it.getNotificationItem(roomId, eventId)?.let { notificationItem -> + notificationMapper.map(notificationItem) + } + } } } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt index a788e56f19e..879a9694a34 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt @@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationService class FakeNotificationService : NotificationService { - override fun getNotification(userId: String, roomId: String, eventId: String): NotificationData? { - return null + override suspend fun getNotification(userId: String, roomId: String, eventId: String): Result { + return Result.success(null) } } From d654f4844468febbd12ea2c6ddac223ecafd9ae6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Mar 2023 22:17:19 +0200 Subject: [PATCH 064/104] Observe session database to be able to detect new user and removed user. --- .../userpushstore/UserPushStoreFactory.kt | 37 +++++++- .../sessionstorage/api/SessionStore.kt | 6 ++ .../api/observer/SessionListener.kt | 22 +++++ .../api/observer/SessionObserver.kt | 22 +++++ .../impl/memory/InMemorySessionStore.kt | 4 + .../impl/DatabaseSessionStore.kt | 15 ++- .../impl/observer/DefaultSessionObserver.kt | 91 +++++++++++++++++++ 7 files changed, 191 insertions(+), 6 deletions(-) create mode 100644 libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionListener.kt create mode 100644 libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionObserver.kt create mode 100644 libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt index 4b16a214915..0323713de72 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt @@ -17,16 +17,43 @@ package io.element.android.libraries.push.impl.userpushstore import android.content.Context +import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.sessionstorage.api.observer.SessionListener +import io.element.android.libraries.sessionstorage.api.observer.SessionObserver import javax.inject.Inject +@SingleIn(AppScope::class) class UserPushStoreFactory @Inject constructor( @ApplicationContext private val context: Context, -) { + private val sessionObserver: SessionObserver, +) : SessionListener { + init { + observeSessions() + } + + // We can have only one class accessing a single data store, so keep a cache of them. + private val cache = mutableMapOf() fun create(userId: String): UserPushStore { - return UserPushStoreDataStore( - context = context, - userId = userId - ) + return cache.getOrPut(userId) { + UserPushStoreDataStore( + context = context, + userId = userId + ) + } + } + + private fun observeSessions() { + sessionObserver.addListener(this) + } + + override suspend fun onSessionCreated(userId: String) { + // Nothing to do + } + + override suspend fun onSessionDeleted(userId: String) { + // Delete the store + create(userId).reset() } } diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt index 223ab16aa55..d79d700030c 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt @@ -17,9 +17,11 @@ package io.element.android.libraries.sessionstorage.api import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map interface SessionStore { fun isLoggedIn(): Flow + fun sessionsFlow(): Flow> suspend fun storeData(sessionData: SessionData) suspend fun getSession(sessionId: String): SessionData? suspend fun getAllSessions(): List @@ -30,3 +32,7 @@ interface SessionStore { fun List.toUserList(): List { return map { it.userId } } + +fun Flow>.toUserListFlow(): Flow> { + return map { it.toUserList() } +} diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionListener.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionListener.kt new file mode 100644 index 00000000000..7bcb4db7926 --- /dev/null +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionListener.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.sessionstorage.api.observer + +interface SessionListener { + suspend fun onSessionCreated(userId: String) + suspend fun onSessionDeleted(userId: String) +} diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionObserver.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionObserver.kt new file mode 100644 index 00000000000..e61b4e2bba5 --- /dev/null +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionObserver.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.sessionstorage.api.observer + +interface SessionObserver { + fun addListener(listener: SessionListener) + fun removeListener(listener: SessionListener) +} diff --git a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt b/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt index ce5b6e24f24..e23e34983c3 100644 --- a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt +++ b/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt @@ -30,6 +30,10 @@ class InMemorySessionStore : SessionStore { return sessionDataFlow.map { it != null } } + override fun sessionsFlow(): Flow> { + return sessionDataFlow.map { listOfNotNull(it) } + } + override suspend fun storeData(sessionData: SessionData) { sessionDataFlow.value = sessionData } diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt index 15c30247125..9394b66e66d 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.sessionstorage.impl import com.squareup.anvil.annotations.ContributesBinding import com.squareup.sqldelight.runtime.coroutines.asFlow +import com.squareup.sqldelight.runtime.coroutines.mapToList import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn @@ -25,6 +26,7 @@ import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import timber.log.Timber import javax.inject.Inject @SingleIn(AppScope::class) @@ -34,7 +36,10 @@ class DatabaseSessionStore @Inject constructor( ) : SessionStore { override fun isLoggedIn(): Flow { - return database.sessionDataQueries.selectFirst().asFlow().mapToOneOrNull().map { it != null } + return database.sessionDataQueries.selectFirst() + .asFlow() + .mapToOneOrNull() + .map { it != null } } override suspend fun storeData(sessionData: SessionData) { @@ -59,6 +64,14 @@ class DatabaseSessionStore @Inject constructor( .map { it.toApiModel() } } + override fun sessionsFlow(): Flow> { + Timber.w("Observing session list!") + return database.sessionDataQueries.selectAll() + .asFlow() + .mapToList() + .map { it.map { sessionData -> sessionData.toApiModel() } } + } + override suspend fun removeSession(sessionId: String) { database.sessionDataQueries.removeSession(sessionId) } diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt new file mode 100644 index 00000000000..dbcfca2d4cb --- /dev/null +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.sessionstorage.impl.observer + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.api.observer.SessionListener +import io.element.android.libraries.sessionstorage.api.observer.SessionObserver +import io.element.android.libraries.sessionstorage.api.toUserListFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.CopyOnWriteArraySet +import javax.inject.Inject + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultSessionObserver @Inject constructor( + private val sessionStore: SessionStore, + private val coroutineScope: CoroutineScope, + private val dispatchers: CoroutineDispatchers, +) : SessionObserver { + // Keep only the userId + private var currentUsers: Set? = null + + init { + observeDatabase() + } + + private val listeners = CopyOnWriteArraySet() + override fun addListener(listener: SessionListener) { + listeners.add(listener) + } + + override fun removeListener(listener: SessionListener) { + listeners.remove(listener) + } + + private fun observeDatabase() { + coroutineScope.launch { + withContext(dispatchers.io) { + sessionStore.sessionsFlow() + .toUserListFlow() + .map { it.toSet() } + .onEach { newUserSet -> + val currentUserSet = currentUsers + if (currentUserSet != null) { + // Compute diff + // Removed user + val removedUsers = currentUserSet - newUserSet + removedUsers.forEach { removedUser -> + listeners.onEach { listener -> + listener.onSessionDeleted(removedUser) + } + } + // Added user + val addedUsers = newUserSet - currentUserSet + addedUsers.forEach { addedUser -> + listeners.onEach { listener -> + listener.onSessionDeleted(addedUser) + } + } + } + + currentUsers = newUserSet + } + .collect() + } + } + } +} From e3d6c297513a3500f2d8aabb7af01aaaf2564280 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Apr 2023 09:42:16 +0200 Subject: [PATCH 065/104] Cleanup --- .../firebase/VectorFirebaseMessagingService.kt | 16 +++++++--------- .../libraries/push/impl/push/PushHandler.kt | 14 ++++++-------- .../VectorUnifiedPushMessagingReceiver.kt | 14 +++++++++----- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt index f0e3bc16091..8769baa9478 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt @@ -21,8 +21,8 @@ import com.google.firebase.messaging.RemoteMessage import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.push.impl.PushersManager -import io.element.android.libraries.push.impl.push.PushHandler import io.element.android.libraries.push.impl.log.pushLoggerTag +import io.element.android.libraries.push.impl.push.PushHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -33,12 +33,8 @@ private val loggerTag = LoggerTag("Firebase", pushLoggerTag) class VectorFirebaseMessagingService : FirebaseMessagingService() { @Inject lateinit var pushersManager: PushersManager - - @Inject - lateinit var pushParser: FirebasePushParser - - @Inject - lateinit var pushHandler: PushHandler + @Inject lateinit var pushParser: FirebasePushParser + @Inject lateinit var pushHandler: PushHandler private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -56,8 +52,10 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { Timber.tag(loggerTag.value).d("New Firebase message") - pushParser.parse(message.data).let { - pushHandler.handle(it) + coroutineScope.launch { + pushParser.parse(message.data).let { + pushHandler.handle(it) + } } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt index 2f7a1947c5f..280fda99e5d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt @@ -32,20 +32,19 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.push.api.store.PushDataStore import io.element.android.libraries.push.impl.PushersManager import io.element.android.libraries.push.impl.clientsecret.PushClientSecret +import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver import io.element.android.libraries.push.impl.notifications.NotificationActionIds import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager -import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.store.DefaultPushDataStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("Push", pushLoggerTag) +private val loggerTag = LoggerTag("PushHandler", pushLoggerTag) class PushHandler @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, @@ -73,16 +72,14 @@ class PushHandler @Inject constructor( * * @param pushData the data received in the push. */ - fun handle(pushData: PushData) { + suspend fun handle(pushData: PushData) { Timber.tag(loggerTag.value).d("## handling pushData") if (buildMeta.lowPrivacyLoggingEnabled) { Timber.tag(loggerTag.value).d("## pushData: $pushData") } - runBlocking { - defaultPushDataStore.incrementPushCounter() - } + defaultPushDataStore.incrementPushCounter() // Diagnostic Push if (pushData.eventId == PushersManager.TEST_EVENT_ID) { @@ -91,6 +88,7 @@ class PushHandler @Inject constructor( return } + // TODO EAx Should be per user if (!pushDataStore.areNotificationEnabledForDevice()) { Timber.tag(loggerTag.value).i("Notification are disabled for this device") return @@ -141,7 +139,7 @@ class PushHandler @Inject constructor( // Restore session val session = matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull() ?: return // TODO EAx, no need for a session? - val notificationData = session.use { + val notificationData = session.let {// TODO Use make the app crashes it.notificationService().getNotification( userId = userId, roomId = pushData.roomId, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index db4489a489a..81dd389e78a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt @@ -23,7 +23,9 @@ import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.push.api.model.BackgroundSyncMode import io.element.android.libraries.push.api.store.PushDataStore -import io.element.android.libraries.push.impl.* +import io.element.android.libraries.push.impl.PushersManager +import io.element.android.libraries.push.impl.UnifiedPushHelper +import io.element.android.libraries.push.impl.UnifiedPushStore import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.push.PushHandler import kotlinx.coroutines.CoroutineScope @@ -64,10 +66,12 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { */ override fun onMessage(context: Context, message: ByteArray, instance: String) { Timber.tag(loggerTag.value).d("New message") - pushParser.parse(message)?.let { - pushHandler.handle(it) - } ?: run { - Timber.tag(loggerTag.value).w("Invalid received data Json format") + coroutineScope.launch { + pushParser.parse(message)?.let { + pushHandler.handle(it) + } ?: run { + Timber.tag(loggerTag.value).w("Invalid received data Json format") + } } } From 1243c114317898952d1e60761e89b5d9eb8f29aa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Apr 2023 11:19:29 +0200 Subject: [PATCH 066/104] Bad copy/paste --- .../sessionstorage/impl/observer/DefaultSessionObserver.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt index dbcfca2d4cb..8fa5d9dd165 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt @@ -77,7 +77,7 @@ class DefaultSessionObserver @Inject constructor( val addedUsers = newUserSet - currentUserSet addedUsers.forEach { addedUser -> listeners.onEach { listener -> - listener.onSessionDeleted(addedUser) + listener.onSessionCreated(addedUser) } } } From 3249420ee7a129f3515a33ab7737ce6439ffb70c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Apr 2023 11:54:40 +0200 Subject: [PATCH 067/104] Fix multi Activity wen opening app from notification. --- app/src/main/AndroidManifest.xml | 6 +++--- .../kotlin/io/element/android/x/MainActivity.kt | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 31033b05009..828788ed805 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - - - No valid Google Play Services APK found. Notifications may not work properly. - Choose how to receive notifications - Google Services - Background synchronization + No valid Google Play Services found. Notifications may not work properly. + Choose how to receive notifications + Google Services + Background synchronization - Listening for events - Noisy notifications - Silent notifications - Call + Listening for events + Noisy notifications + Silent notifications + Call + Me New Messages - Mark as read - Join - Reject - You are viewing the notification! Click me! + Mark as read + Quick reply + Join + Reject + You are viewing the notification! Click me! %1$s: %2$s %1$s: %2$s %3$s ** Failed to send - please open room %1$s in %2$s and %3$s" %1$s and %2$s" %1$s in %2$s" - + %d new message %d new messages From ec35d2ce03cb4770032770e52bccb6c6b0ca1cd5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 14:50:06 +0200 Subject: [PATCH 077/104] Add strings to localazy and import them --- .../libraries/push/impl/UnifiedPushHelper.kt | 10 +-- .../impl/src/main/res/values/localazy.xml | 48 ++++++++++++++ .../impl/src/main/res/values/temporary.xml | 64 ------------------- .../src/main/res/values/localazy.xml | 1 + tools/localazy/config.json | 7 ++ 5 files changed, 61 insertions(+), 69 deletions(-) create mode 100644 libraries/push/impl/src/main/res/values/localazy.xml delete mode 100644 libraries/push/impl/src/main/res/values/temporary.xml diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt index adabca5b188..12ed3f1993c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt @@ -44,9 +44,9 @@ class UnifiedPushHelper @Inject constructor( ) { val internalDistributorName = stringProvider.getString( if (fcmHelper.isFirebaseAvailable()) { - R.string.push_distributor_firebase + R.string.push_distributor_firebase_android } else { - R.string.push_distributor_background_sync + R.string.push_distributor_background_sync_android } ) @@ -60,7 +60,7 @@ class UnifiedPushHelper @Inject constructor( } MaterialAlertDialogBuilder(context) - .setTitle(stringProvider.getString(R.string.push_choose_distributor_dialog_title)) + .setTitle(stringProvider.getString(R.string.push_choose_distributor_dialog_title_android)) .setItems(distributorsName.toTypedArray()) { _, which -> val distributor = distributors[which] onDistributorSelected(distributor) @@ -133,8 +133,8 @@ class UnifiedPushHelper @Inject constructor( fun getCurrentDistributorName(): String { return when { - isEmbeddedDistributor() -> stringProvider.getString(R.string.push_distributor_firebase) - isBackgroundSync() -> stringProvider.getString(R.string.push_distributor_background_sync) + isEmbeddedDistributor() -> stringProvider.getString(R.string.push_distributor_firebase_android) + isBackgroundSync() -> stringProvider.getString(R.string.push_distributor_background_sync_android) else -> context.getApplicationLabel(UnifiedPush.getDistributor(context)) } } diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml new file mode 100644 index 00000000000..3a11adb5d3b --- /dev/null +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -0,0 +1,48 @@ + + + "Call" + "Listening for events" + "Noisy notifications" + "Silent notifications" + "** Failed to send - please open room" + "Join" + "Reject" + "New Messages" + "Mark as read" + "Quick reply" + "Me" + "You are viewing the notification! Click me!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + "%1$s and %2$s" + "%1$s in %2$s" + "%1$s in %2$s and %3$s" + + "%1$s: %2$d message" + "%1$s: %2$d messages" + + + "%d notification" + "%d notifications" + + + "%d invitation" + "%d invitations" + + + "%d new message" + "%d new messages" + + + "%d unread notified message" + "%d unread notified messages" + + + "%d room" + "%d rooms" + + "Choose how to receive notifications" + "Background synchronization" + "Google Services" + "No valid Google Play Services found. Notifications may not work properly." + \ No newline at end of file diff --git a/libraries/push/impl/src/main/res/values/temporary.xml b/libraries/push/impl/src/main/res/values/temporary.xml deleted file mode 100644 index e0833bd2dcb..00000000000 --- a/libraries/push/impl/src/main/res/values/temporary.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - No valid Google Play Services found. Notifications may not work properly. - Choose how to receive notifications - Google Services - Background synchronization - - Listening for events - Noisy notifications - Silent notifications - Call - Me - New Messages - Mark as read - Quick reply - Join - Reject - You are viewing the notification! Click me! - %1$s: %2$s - %1$s: %2$s %3$s - ** Failed to send - please open room - %1$s in %2$s and %3$s" - %1$s and %2$s" - %1$s in %2$s" - - %d new message - %d new messages - - - %d unread notified message - %d unread notified messages - - - %d room - %d rooms - - - %d invitation - %d invitations - - - %1$s: %2$d message - %1$s: %2$d messages - - - %d notification - %d notifications - - diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 28cef98c609..c37935d35bb 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -38,6 +38,7 @@ "Save" "Search" "Send" + "Send message" "Share" "Share link" "Skip" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 155a42836a3..59c6bd69116 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -43,6 +43,13 @@ "rich_text_editor_.*" ] }, + { + "name": ":libraries:push:impl", + "includeRegex": [ + "push_.*", + "notification_.*" + ] + }, { "name": ":features:login:impl", "includeRegex": [ From 9ee90a0dae5e3963519c6d3368e6bc4cab484358 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 15:13:34 +0200 Subject: [PATCH 078/104] Fix lint warnings. --- .../intent/PendingIntentCompat.kt | 30 ----------------- .../impl/notifications/NotificationUtils.kt | 33 ++++++++++++------- 2 files changed, 21 insertions(+), 42 deletions(-) delete mode 100644 libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/intent/PendingIntentCompat.kt diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/intent/PendingIntentCompat.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/intent/PendingIntentCompat.kt deleted file mode 100644 index dcdb800a199..00000000000 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/intent/PendingIntentCompat.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2021 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.libraries.androidutils.intent - -import android.app.PendingIntent -import android.os.Build - -object PendingIntentCompat { - const val FLAG_IMMUTABLE = PendingIntent.FLAG_IMMUTABLE - - val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - PendingIntent.FLAG_MUTABLE - } else { - 0 - } -} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index 28a98f496f3..eeb02ebb8bf 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl.notifications +import android.Manifest import android.annotation.SuppressLint import android.app.Activity import android.app.Notification @@ -26,18 +27,19 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.Canvas import android.os.Build import androidx.annotation.ChecksSdkIntAtLeast import androidx.annotation.DrawableRes +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.RemoteInput import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.content.res.ResourcesCompat -import io.element.android.libraries.androidutils.intent.PendingIntentCompat import io.element.android.libraries.androidutils.system.startNotificationChannelSettingsIntent import io.element.android.libraries.androidutils.uri.createIgnoredUri import io.element.android.libraries.core.meta.BuildMeta @@ -295,7 +297,7 @@ class NotificationUtils @Inject constructor( context, clock.epochMillis().toInt(), markRoomReadIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) NotificationCompat.Action.Builder( @@ -341,7 +343,7 @@ class NotificationUtils @Inject constructor( context.applicationContext, clock.epochMillis().toInt(), intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) setDeleteIntent(pendingIntent) } @@ -377,7 +379,7 @@ class NotificationUtils @Inject constructor( context, clock.epochMillis().toInt(), rejectIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) addAction( @@ -396,7 +398,7 @@ class NotificationUtils @Inject constructor( context, clock.epochMillis().toInt(), joinIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) addAction( R.drawable.vector_notification_accept_invitation, @@ -489,7 +491,7 @@ class NotificationUtils @Inject constructor( context, clock.epochMillis().toInt(), roomIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } @@ -505,7 +507,7 @@ class NotificationUtils @Inject constructor( context, clock.epochMillis().toInt(), threadIntentTap, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } @@ -516,7 +518,7 @@ class NotificationUtils @Inject constructor( context, clock.epochMillis().toInt(), intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } @@ -549,7 +551,11 @@ class NotificationUtils @Inject constructor( clock.epochMillis().toInt(), intent, // PendingIntents attached to actions with remote inputs must be mutable - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_MUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE + } else { + 0 + } ) } else { /* @@ -627,7 +633,7 @@ class NotificationUtils @Inject constructor( context.applicationContext, 0, intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } @@ -652,15 +658,18 @@ class NotificationUtils @Inject constructor( @SuppressLint("LaunchActivityFromNotification") fun displayDiagnosticNotification() { + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + Timber.w("Not allowed to notify.") + return + } val testActionIntent = Intent(context, TestNotificationReceiver::class.java) testActionIntent.action = actionIds.diagnostic val testPendingIntent = PendingIntent.getBroadcast( context, 0, testActionIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) - notificationManager.notify( "DIAGNOSTIC", 888, From 676faca50ac35196cef14180095923b96c025277 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 15:20:56 +0200 Subject: [PATCH 079/104] Add test for NoopPermissionsPresenter and remove the unused factory. --- libraries/permissions/noop/build.gradle.kts | 6 +++ .../noop/NoopPermissionsPresenterFactory.kt | 23 ---------- .../noop/NoopPermissionsPresenterTest.kt | 45 +++++++++++++++++++ 3 files changed, 51 insertions(+), 23 deletions(-) delete mode 100644 libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterFactory.kt create mode 100644 libraries/permissions/noop/src/test/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterTest.kt diff --git a/libraries/permissions/noop/build.gradle.kts b/libraries/permissions/noop/build.gradle.kts index 7319f73104b..8a1949814f7 100644 --- a/libraries/permissions/noop/build.gradle.kts +++ b/libraries/permissions/noop/build.gradle.kts @@ -27,4 +27,10 @@ android { dependencies { implementation(projects.libraries.architecture) api(projects.libraries.permissions.api) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) } diff --git a/libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterFactory.kt b/libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterFactory.kt deleted file mode 100644 index b9829694831..00000000000 --- a/libraries/permissions/noop/src/main/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterFactory.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2023 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 io.element.android.libraries.permissions.noop - -import io.element.android.libraries.permissions.api.PermissionsPresenter - -class NoopPermissionsPresenterFactory : PermissionsPresenter.Factory { - override fun create(permission: String) = NoopPermissionsPresenter() -} diff --git a/libraries/permissions/noop/src/test/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterTest.kt b/libraries/permissions/noop/src/test/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterTest.kt new file mode 100644 index 00000000000..36b908cb0d8 --- /dev/null +++ b/libraries/permissions/noop/src/test/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.libraries.permissions.noop + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class NoopPermissionsPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = NoopPermissionsPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.permission).isEmpty() + assertThat(initialState.permissionGranted).isFalse() + assertThat(initialState.shouldShowRationale).isFalse() + assertThat(initialState.permissionAlreadyAsked).isFalse() + assertThat(initialState.permissionAlreadyDenied).isFalse() + assertThat(initialState.showDialog).isFalse() + } + } +} From b752910dd955ff3b0cfcf6821086931381a21323 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 15:28:05 +0200 Subject: [PATCH 080/104] Comment unused code. --- .../element/android/appnav/loggedin/LoggedInEvents.kt | 7 +++---- .../android/appnav/loggedin/LoggedInPresenter.kt | 11 +++++------ .../element/android/appnav/loggedin/LoggedInState.kt | 2 +- .../android/appnav/loggedin/LoggedInStateProvider.kt | 2 +- .../permissions/impl/DefaultPermissionsPresenter.kt | 2 ++ 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt index 2712b420033..664ec1f663d 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt @@ -16,7 +16,6 @@ package io.element.android.appnav.loggedin -// TODO Add your events or remove the file completely if no events -sealed interface LoggedInEvents { - object MyEvent : LoggedInEvents -} +// sealed interface LoggedInEvents { +// object MyEvent : LoggedInEvents +// } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index 82f927a7430..b628f19bd17 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -51,15 +51,14 @@ class LoggedInPresenter @Inject constructor( val permissionsState = postNotificationPermissionsPresenter.present() - fun handleEvents(event: LoggedInEvents) { - when (event) { - LoggedInEvents.MyEvent -> Unit - } - } + // fun handleEvents(event: LoggedInEvents) { + // when (event) { + // } + // } return LoggedInState( permissionsState = permissionsState, - eventSink = ::handleEvents + // eventSink = ::handleEvents ) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt index a5c43801bd9..8cf8060981d 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt @@ -20,5 +20,5 @@ import io.element.android.libraries.permissions.api.PermissionsState data class LoggedInState( val permissionsState: PermissionsState, - val eventSink: (LoggedInEvents) -> Unit + // val eventSink: (LoggedInEvents) -> Unit ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt index 90ff2136e57..b131d6d6104 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt @@ -29,5 +29,5 @@ open class LoggedInStateProvider : PreviewParameterProvider { fun aLoggedInState() = LoggedInState( permissionsState = createDummyPostNotificationPermissionsState(), - eventSink = {} + // eventSink = {} ) diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt index c09a595783f..c422cd1ec9b 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt @@ -127,6 +127,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor( } } + /* @Composable private fun resetStore() { LaunchedEffect(this@DefaultPermissionsPresenter) { @@ -135,4 +136,5 @@ class DefaultPermissionsPresenter @AssistedInject constructor( } } } + */ } From e41aa74e6ccebd0cbef7a5a664f6ba2ae4bce87d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 15:44:11 +0200 Subject: [PATCH 081/104] Add test for LoggedInPresenter --- .../appnav/loggedin/LoggedInPresenterTest.kt | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt new file mode 100644 index 00000000000..64767eaafca --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.appnav.loggedin + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter +import io.element.android.libraries.push.api.PushService +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LoggedInPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.permissionsState.permission).isEmpty() + } + } + + private fun createPresenter(): LoggedInPresenter { + return LoggedInPresenter( + matrixClient = FakeMatrixClient(), + permissionsPresenterFactory = object : PermissionsPresenter.Factory { + override fun create(permission: String): PermissionsPresenter { + return NoopPermissionsPresenter() + } + }, + pushService = object : PushService { + override fun notificationStyleChanged() { + } + + override suspend fun registerFirebasePusher(matrixClient: MatrixClient) { + } + + override suspend fun testPush() { + } + } + ) + } +} From 0efdcd96d1ed57f737b0e2603521f16ad5c69e0d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 15:50:04 +0200 Subject: [PATCH 082/104] Ignore lint warning. I think it's OK. --- libraries/push/impl/src/main/AndroidManifest.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/push/impl/src/main/AndroidManifest.xml b/libraries/push/impl/src/main/AndroidManifest.xml index 1c380c260ae..a386a89caf0 100644 --- a/libraries/push/impl/src/main/AndroidManifest.xml +++ b/libraries/push/impl/src/main/AndroidManifest.xml @@ -14,7 +14,8 @@ ~ limitations under the License. --> - + @@ -37,7 +38,8 @@ + android:exported="true" + tools:ignore="ExportedReceiver"> From cd31a8fd72de7d0746198a447eea0dc53d59beeb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 15:55:50 +0200 Subject: [PATCH 083/104] Fix lint warnings. --- .../android/libraries/androidutils/system/SystemUtils.kt | 8 +++++--- .../permissions/impl/DefaultPermissionsPresenter.kt | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt index 8f01f285450..5f18f468275 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.androidutils.system +import android.annotation.SuppressLint import android.annotation.TargetApi import android.app.Activity import android.content.ActivityNotFoundException @@ -77,6 +78,7 @@ fun Context.getApplicationLabel(packageName: String): String { * Note: If the user finally does not grant the permission, PushManager.isBackgroundSyncAllowed() * will return false and the notification privacy will fallback to "LOW_DETAIL". */ +@SuppressLint("BatteryLife") fun requestDisablingBatteryOptimization(activity: Activity, activityResultLauncher: ActivityResultLauncher) { val intent = Intent() intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS @@ -114,9 +116,9 @@ fun startNotificationSettingsIntent(context: Context, activityResultLauncher: Ac intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) } else { - intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS - intent.putExtra("app_package", context.packageName) - intent.putExtra("app_uid", context.applicationInfo?.uid) + intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.data = Uri.fromParts("package", context.packageName, null) } activityResultLauncher.launch(intent) } diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt index c422cd1ec9b..9e91869549b 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt @@ -58,7 +58,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor( override fun present(): PermissionsState { val localCoroutineScope = rememberCoroutineScope() - // To reset the store: resetStore() + // To reset the store: ResetStore() val isAlreadyDenied: Boolean by permissionsStore .isPermissionDenied(permission) @@ -129,7 +129,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor( /* @Composable - private fun resetStore() { + private fun ResetStore() { LaunchedEffect(this@DefaultPermissionsPresenter) { launch { permissionsStore.resetStore() From 4f5543bca2db9a01fc50717cc0eb0f358090f20b Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 17:33:52 +0200 Subject: [PATCH 084/104] Remove RetryStartDM action --- .../features/createroom/impl/root/CreateRoomRootEvents.kt | 1 - .../createroom/impl/root/CreateRoomRootPresenter.kt | 4 ---- .../features/createroom/impl/root/CreateRoomRootView.kt | 6 +++++- .../createroom/impl/root/CreateRoomRootPresenterTests.kt | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt index de9810f34ad..87f35e4b1b2 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt @@ -21,6 +21,5 @@ import io.element.android.libraries.matrix.ui.model.MatrixUser sealed interface CreateRoomRootEvents { object InvitePeople : CreateRoomRootEvents data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents - object RetryStartDM : CreateRoomRootEvents object CancelStartDM : CreateRoomRootEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 43ca377771c..89932b0cdbd 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -69,10 +69,6 @@ class CreateRoomRootPresenter @Inject constructor( fun handleEvents(event: CreateRoomRootEvents) { when (event) { is CreateRoomRootEvents.StartDM -> startDm(event.matrixUser) - CreateRoomRootEvents.RetryStartDM -> { - startDmAction.value = Async.Uninitialized - userListState.selectedUsers.firstOrNull()?.let { startDm(it) } - } CreateRoomRootEvents.CancelStartDM -> startDmAction.value = Async.Uninitialized CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 146c4479967..61b8b8738e4 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -112,7 +112,11 @@ fun CreateRoomRootView( RetryDialog( content = stringResource(id = StringR.string.screen_start_chat_error_starting_chat), onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) }, - onRetry = { state.eventSink(CreateRoomRootEvents.RetryStartDM) }, + onRetry = { + state.userListState.selectedUsers.firstOrNull()?.let { + state.eventSink(CreateRoomRootEvents.StartDM(it)) + } + }, ) } else -> Unit diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index f1467485db1..cf399fdbd32 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -145,7 +145,7 @@ class CreateRoomRootPresenterTests { // Retry with success fakeMatrixClient.givenCreateDmError(null) - stateAfterSecondAttempt.eventSink(CreateRoomRootEvents.RetryStartDM) + stateAfterSecondAttempt.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) assertThat(awaitItem().startDmAction).isInstanceOf(Async.Uninitialized::class.java) assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) val stateAfterRetryStartDM = awaitItem() From 6f1e9a618200868acedefab6f7b2eb25371f35d7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 17:37:09 +0200 Subject: [PATCH 085/104] Cancel start DM if there is no more selected user --- .../features/createroom/impl/root/CreateRoomRootView.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 61b8b8738e4..e488645b785 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -113,9 +113,10 @@ fun CreateRoomRootView( content = stringResource(id = StringR.string.screen_start_chat_error_starting_chat), onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) }, onRetry = { - state.userListState.selectedUsers.firstOrNull()?.let { - state.eventSink(CreateRoomRootEvents.StartDM(it)) - } + state.userListState.selectedUsers.firstOrNull() + ?.let { state.eventSink(CreateRoomRootEvents.StartDM(it)) } + // Cancel start DM if there is no more selected user (should not happen) + ?: state.eventSink(CreateRoomRootEvents.CancelStartDM) }, ) } From e53ac50e8ab46623ccfd83343e71298e1c0ec300 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 18:14:32 +0200 Subject: [PATCH 086/104] Fix wildcard import --- .../permissions/impl/FakePermissionStateProvider.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt index 2c670618114..c204ff5fc6c 100644 --- a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt @@ -18,7 +18,11 @@ package io.element.android.libraries.permissions.impl -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.PermissionStatus From 2d63e25aceb1e24c46ab2a88b3bf8042e7f580d5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 20:28:50 +0000 Subject: [PATCH 087/104] Update accompanist --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 80f67bab6bd..f6966b86d2f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ composecompiler = "1.4.2" coroutines = "1.6.4" # Accompanist -accompanist = "0.28.0" +accompanist = "0.30.1" # Test test_core = "1.5.0" From ad91430595f764a42ca97020d01958a614f82ea8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 20:29:03 +0000 Subject: [PATCH 088/104] Update dependency androidx.core:core-ktx to v1.10.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 80f67bab6bd..1c09366deca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ molecule = "0.8.0" # AndroidX material = "1.8.0" -corektx = "1.9.0" +corektx = "1.10.0" datastore = "1.0.0" constraintlayout = "2.1.4" recyclerview = "1.3.0" From 8c916ed49d9af768bd161934e13cad688f5c72f1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 6 Apr 2023 09:12:44 +0200 Subject: [PATCH 089/104] Ignore some classes about coverage. --- build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 38a11235dfc..7477d4da731 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -231,6 +231,7 @@ koverMerged { overrideClassFilter { includes += "*Presenter" excludes += "*TemplatePresenter" + excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*" } bound { minValue = 90 @@ -247,6 +248,7 @@ koverMerged { excludes += "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*" excludes += "io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*" excludes += "io.element.android.libraries.matrix.api.room.RoomMembershipState*" + excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*" } bound { minValue = 90 From cdacfbf2842b5a87d2446dece29348bf8926f4a6 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 6 Apr 2023 11:30:46 +0200 Subject: [PATCH 090/104] Update compose BOM version --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f6966b86d2f..184798d0cd4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ activity = "1.7.0" startup = "1.1.1" # Compose -compose_bom = "2023.01.00" +compose_bom = "2023.03.00" composecompiler = "1.4.2" # Coroutines From 33dcd64b5bc1c8f4220c966c6d0afd482ccd9037 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 6 Apr 2023 11:34:31 +0200 Subject: [PATCH 091/104] update screenshots --- ...dalBottomSheetLayoutDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...alBottomSheetLayoutLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutDarkPreview_0_null,NEXUS_5,1.0,en].png index 1be155bb969..a4aa94b6e4a 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67a8353194965d1139b558b848f1d1a5411370555eefc765c055ad8a126c4265 -size 6117 +oid sha256:ddbb6611ae83055106f7b67ec828542f8a896cafb49001ed0baef43633cc77c1 +size 8884 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutLightPreview_0_null,NEXUS_5,1.0,en].png index 38b159ccdf4..317643c5984 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:694fda9bd548990ef322fe3db691ccb7973c5d5efb8e78d6e50030109dc96359 -size 5947 +oid sha256:5c9a3c9f68a6627654856b03d2534ae1e4e8e600989bc3719407fbf8e17a7ab1 +size 8631 From ec562ee912a8f592867e1b4917f7e05cd3ecc4a4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 10:38:47 +0200 Subject: [PATCH 092/104] Fix ktlint issue. --- libraries/permissions/impl/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/permissions/impl/build.gradle.kts b/libraries/permissions/impl/build.gradle.kts index 43608d36e89..84cd5315285 100644 --- a/libraries/permissions/impl/build.gradle.kts +++ b/libraries/permissions/impl/build.gradle.kts @@ -60,7 +60,6 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) - androidTestImplementation(libs.test.junitext) ksp(libs.showkase.processor) From 14e6ca87cfc96b91c961bed2a1a00dab09b6f18e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 12:26:25 +0200 Subject: [PATCH 093/104] Setup localazy before running it. From https://localazy.com/docs/cli/installation#debianubuntu --- .github/workflows/sync-localazy.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index e3c745b8f6d..afb5fe9f751 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -15,6 +15,11 @@ jobs: uses: actions/setup-python@v4 with: python-version: 3.8 + - name: Setup Localazy + run: | + curl -sS https://dist.localazy.com/debian/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/localazy.gpg + echo "deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/localazy.gpg] https://maven.localazy.com/repository/apt/ stable main" | sudo tee /etc/apt/sources.list.d/localazy.list + sudo apt-get update && sudo apt-get install localazy - name: Run Localazy script run: ./tools/localazy/downloadStrings.sh --all - name: Create Pull Request for Strings From f3c4f356dd8a7d6c3153966762f2623e439cd518 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 12:29:35 +0200 Subject: [PATCH 094/104] Run every 10 minutes to check the script. (to be reverted!) --- .github/workflows/sync-localazy.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index afb5fe9f751..f171a506f10 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -2,7 +2,8 @@ name: Sync Localazy on: schedule: # At 00:00 on every Monday UTC - - cron: '0 0 * * 1' + # - cron: '0 0 * * 1' + - cron: '0/10 * * * *' jobs: sync-localazy: From 878158ba9b11adbbc0689b050cb72c8fbf713272 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 14:39:13 +0200 Subject: [PATCH 095/104] For usage of Python3 CI complain with: Traceback (most recent call last): File "./tools/localazy/generateLocalazyConfig.py", line 39, in action = baseAction | { TypeError: unsupported operand type(s) for |: 'dict' and 'dict' --- tools/localazy/downloadStrings.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/localazy/downloadStrings.sh b/tools/localazy/downloadStrings.sh index 5892a87c388..0f35f9ec596 100755 --- a/tools/localazy/downloadStrings.sh +++ b/tools/localazy/downloadStrings.sh @@ -27,7 +27,7 @@ else fi echo "Generating the configuration file for localazy..." -./tools/localazy/generateLocalazyConfig.py $allFiles +python3 ./tools/localazy/generateLocalazyConfig.py $allFiles echo "Deleting all existing localazy.xml files..." find . -name 'localazy.xml' -delete From fc216aebf87a9ffe3a2ea79773ef3a57e3b68e87 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:01:34 +0200 Subject: [PATCH 096/104] Use Python 3.9 CI complain with: Traceback (most recent call last): File "./tools/localazy/generateLocalazyConfig.py", line 39, in action = baseAction | { TypeError: unsupported operand type(s) for |: 'dict' and 'dict' --- .github/workflows/sync-localazy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index f171a506f10..2b44c603525 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'vector-im/element-x-android' steps: - uses: actions/checkout@v3 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v4 with: python-version: 3.8 From 4708bb0a720776ed3e93d51c10daf5af98d87710 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:03:02 +0200 Subject: [PATCH 097/104] Use Python 3.9 CI complain with: Traceback (most recent call last): File "./tools/localazy/generateLocalazyConfig.py", line 39, in action = baseAction | { TypeError: unsupported operand type(s) for |: 'dict' and 'dict' --- .github/workflows/sync-localazy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index 2b44c603525..e5c37338720 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Python 3.9 uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - name: Setup Localazy run: | curl -sS https://dist.localazy.com/debian/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/localazy.gpg From e726cb9f5685fbd5d4d22e4c146594989b281c39 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 12:29:35 +0200 Subject: [PATCH 098/104] Revert "Run every 10 minutes to check the script. (to be reverted!)" This reverts commit f3c4f356dd8a7d6c3153966762f2623e439cd518. --- .github/workflows/sync-localazy.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index e5c37338720..02f93cc3e1b 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -2,8 +2,7 @@ name: Sync Localazy on: schedule: # At 00:00 on every Monday UTC - # - cron: '0 0 * * 1' - - cron: '0/10 * * * *' + - cron: '0 0 * * 1' jobs: sync-localazy: From 7fc2a768598854308e9a93fb0481614d07e04b71 Mon Sep 17 00:00:00 2001 From: Hans Christian Schmitz Date: Mon, 10 Apr 2023 08:21:57 +0200 Subject: [PATCH 099/104] Add support for autofilling login text fields (#293) * Add support for autofilling login text fields Support autofilling login text fields via Android Autofill with the experimental AndroidX Compose API for it. Based on https://bryanherbst.com/2021/04/13/compose-autofill/ (with permission). Signed-off-by: Hans Christian Schmitz * Move autofill implementation to designsystem library Signed-off-by: Hans Christian Schmitz --------- Signed-off-by: Hans Christian Schmitz --- .../features/login/impl/root/LoginRootView.kt | 16 ++++++++-- .../theme/components/TextField.kt | 32 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt index 290aace3d59..b7491f5b3b5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt @@ -49,7 +49,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.AutofillType import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.platform.LocalFocusManager @@ -77,6 +79,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.designsystem.theme.components.autofill import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.testtags.TestTags @@ -206,6 +209,7 @@ internal fun ChangeServerSection( } } +@OptIn(ExperimentalComposeUiApi::class) @Composable internal fun LoginForm( state: LoginRootState, @@ -239,7 +243,11 @@ internal fun LoginForm( modifier = Modifier .fillMaxWidth() .onTabOrEnterKeyFocusNext(focusManager) - .testTag(TestTags.loginEmailUsername), + .testTag(TestTags.loginEmailUsername) + .autofill(autofillTypes = listOf(AutofillType.Username), onFill = { + loginFieldState = it + eventSink(LoginRootEvents.SetLogin(it)) + }), label = { Text(text = stringResource(R.string.screen_login_username_hint)) }, @@ -279,7 +287,11 @@ internal fun LoginForm( modifier = Modifier .fillMaxWidth() .onTabOrEnterKeyFocusNext(focusManager) - .testTag(TestTags.loginPassword), + .testTag(TestTags.loginPassword) + .autofill(autofillTypes = listOf(AutofillType.Password), onFill = { + passwordFieldState = it + eventSink(LoginRootEvents.SetPassword(it)) + }), onValueChange = { passwordFieldState = it eventSink(LoginRootEvents.SetPassword(it)) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt index 4884217b0ce..b2abb1a3736 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt @@ -29,8 +29,17 @@ import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.AutofillNode +import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.composed +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalAutofill +import androidx.compose.ui.platform.LocalAutofillTree import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview @@ -118,3 +127,26 @@ private fun ContentToPreview() { } } } + +@OptIn(ExperimentalComposeUiApi::class) +fun Modifier.autofill(autofillTypes: List, onFill: (String) -> Unit) = composed { + val autofillNode = AutofillNode(autofillTypes, onFill = onFill) + LocalAutofillTree.current += autofillNode + + val autofill = LocalAutofill.current + + this + .onGloballyPositioned { + // Inform autofill framework of where our composable is so it can show the popup in the right place + autofillNode.boundingBox = it.boundsInWindow() + } + .onFocusChanged { + autofill?.run { + if (it.isFocused) { + requestAutofillForNode(autofillNode) + } else { + cancelAutofillForNode(autofillNode) + } + } + } +} From 73b3fde01c6dec05a10c3e9fe578efc50ad9ed2c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 08:33:58 +0200 Subject: [PATCH 100/104] Update dependency com.squareup:kotlinpoet to v1.13.0 (#304) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- anvilcodegen/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anvilcodegen/build.gradle.kts b/anvilcodegen/build.gradle.kts index e8b6ab285c8..d35051d2cc1 100644 --- a/anvilcodegen/build.gradle.kts +++ b/anvilcodegen/build.gradle.kts @@ -25,7 +25,7 @@ dependencies { implementation(projects.anvilannotations) api(libs.anvil.compiler.api) implementation(libs.anvil.compiler.utils) - implementation("com.squareup:kotlinpoet:1.12.0") + implementation("com.squareup:kotlinpoet:1.13.0") implementation(libs.dagger) compileOnly("com.google.auto.service:auto-service-annotations:1.0.1") kapt("com.google.auto.service:auto-service:1.0.1") From dcd9370fe3019953cd9a3df7751648eb7afffa7c Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 10 Apr 2023 09:00:02 +0200 Subject: [PATCH 101/104] Fix lint issues that prevented CI from passing (#310) --- .../libraries/push/impl/GoogleFcmHelper.kt | 3 ++- .../libraries/push/impl/PushersManager.kt | 6 +++--- .../libraries/push/impl/UnifiedPushStore.kt | 2 +- .../PushClientSecretStoreDataStore.kt | 2 +- .../impl/notifications/FilteredEventDetector.kt | 3 ++- .../notifications/NotifiableEventProcessor.kt | 6 +++++- .../notifications/NotifiableEventResolver.kt | 5 +++-- .../impl/notifications/NotificationAction.kt | 3 ++- .../notifications/NotificationBitmapLoader.kt | 2 +- .../NotificationBroadcastReceiver.kt | 4 ++-- .../notifications/NotificationDrawerManager.kt | 3 ++- .../impl/notifications/NotificationRenderer.kt | 3 ++- .../push/impl/notifications/NotificationUtils.kt | 2 +- .../impl/notifications/OutdatedEventDetector.kt | 3 ++- .../impl/notifications/RoomEventGroupInfo.kt | 2 +- .../impl/notifications/model/NotifiableEvent.kt | 3 ++- .../android/libraries/push/impl/push/PushData.kt | 1 + .../push/impl/store/DefaultPushDataStore.kt | 16 ++++++++++++++++ .../push/impl/unifiedpush/GuardServiceStarter.kt | 4 +--- .../push/impl/userpushstore/UserPushStore.kt | 2 +- .../fake/FakeNotificationFactory.kt | 6 +++++- .../appnavstate/test/AppNavStateFixture.kt | 6 +++++- 22 files changed, 61 insertions(+), 26 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt index 0ead18c75e7..6c736071965 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018 New Vector Ltd + * Copyright (c) 2023 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.element.android.libraries.push.impl import android.content.Context diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index f8f445a6df9..f1ac3469090 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2023 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -128,7 +128,7 @@ class PushersManager @Inject constructor( ) /** - * Ex: {"cs":"sfvsdv"} + * Ex: {"cs":"sfvsdv"}. */ private fun createDefaultPayload(secretForUser: String): String { return "{\"cs\":\"$secretForUser\"}" diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.kt index 5e2e33d7d3e..226d0c56695 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.kt @@ -24,7 +24,7 @@ import io.element.android.libraries.di.DefaultPreferences import javax.inject.Inject /** - * TODO EAx Store in BDD (for multisession) + * TODO EAx Store in BDD (for multisession). */ class UnifiedPushStore @Inject constructor( @ApplicationContext val context: Context, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStoreDataStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStoreDataStore.kt index 98b64d4e0ba..055de6fc477 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStoreDataStore.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStoreDataStore.kt @@ -54,7 +54,7 @@ class PushClientSecretStoreDataStore @Inject constructor( override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? { val keyValues = context.dataStore.data.first().asMap() - val matchingKey = keyValues.keys.firstOrNull { + val matchingKey = keyValues.keys.find { keyValues[it] == clientSecret } return matchingKey?.name?.asSessionId() diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FilteredEventDetector.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FilteredEventDetector.kt index 6e92c2ec602..a24f088998e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FilteredEventDetector.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FilteredEventDetector.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 New Vector Ltd + * Copyright (c) 2023 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.element.android.libraries.push.impl.notifications import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt index 7c083555e4c..1b1fe2723e0 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt @@ -16,7 +16,11 @@ package io.element.android.libraries.push.impl.notifications -import io.element.android.libraries.push.impl.notifications.model.* +import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreMessageEventInRoom import io.element.android.services.appnavstate.api.AppNavigationState import timber.log.Timber import javax.inject.Inject diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index 6463773e7bf..5ae39e11029 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2023 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.element.android.libraries.push.impl.notifications import io.element.android.libraries.core.log.logger.LoggerTag @@ -104,7 +105,7 @@ private fun NotificationData.asNotifiableEvent(userId: SessionId, roomId: RoomId } /** - * TODO This is a temporary method for EAx + * TODO This is a temporary method for EAx. */ private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData { return this ?: NotificationData( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationAction.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationAction.kt index 31ec28d0230..b3f0b1e0f26 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationAction.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationAction.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2023 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.element.android.libraries.push.impl.notifications data class NotificationAction( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt index d0ab0237894..7bd76f9f427 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt index 078de3a8a83..a9c7036418f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2023 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt index 4688a4a24c2..8d3bfda4c36 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2023 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.element.android.libraries.push.impl.notifications import android.content.Context diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt index 521f4f3c8c6..277dc3b8223 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2023 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.element.android.libraries.push.impl.notifications import androidx.annotation.WorkerThread diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index eeb02ebb8bf..add8fd74eb5 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/OutdatedEventDetector.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/OutdatedEventDetector.kt index 1d82fc31e44..5b15dc78d2f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/OutdatedEventDetector.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/OutdatedEventDetector.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2023 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.element.android.libraries.push.impl.notifications import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt index b2a5fcbedb7..734c34b051d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt index 40d84496a5f..b1bb7cd032c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2023 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.element.android.libraries.push.impl.notifications.model import io.element.android.libraries.matrix.api.core.EventId diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt index 31d3eb7ea00..864155e5222 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.core.RoomId * @property eventId The Event ID. If not null, it will not be empty, and will have a valid format. * @property roomId The Room ID. If not null, it will not be empty, and will have a valid format. * @property unread Number of unread message. + * @property clientSecret A client secret, used to determine which user should receive the notification. */ data class PushData( val eventId: EventId?, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt index 8474682ab65..ffbd575aa4f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.push.impl.store import android.content.Context diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/GuardServiceStarter.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/GuardServiceStarter.kt index 4c93b8a9291..08bd4a83263 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/GuardServiceStarter.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/GuardServiceStarter.kt @@ -26,6 +26,4 @@ interface GuardServiceStarter { } @ContributesBinding(AppScope::class) -class NoopGuardServiceStarter @Inject constructor() : GuardServiceStarter { - -} +class NoopGuardServiceStarter @Inject constructor() : GuardServiceStarter diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt index a66b2835193..82c4beaf20f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt @@ -24,7 +24,7 @@ const val NOTIFICATION_METHOD_UNIFIEDPUSH = "NOTIFICATION_METHOD_UNIFIEDPUSH" */ interface UserPushStore { /** - * NOTIFICATION_METHOD_FIREBASE or NOTIFICATION_METHOD_UNIFIEDPUSH + * [NOTIFICATION_METHOD_FIREBASE] or [NOTIFICATION_METHOD_UNIFIEDPUSH]. */ suspend fun getNotificationMethod(): String diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt index d9ba17c28e1..7d7812e6cb6 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt @@ -17,7 +17,11 @@ package io.element.android.libraries.push.impl.notifications.fake import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.push.impl.notifications.* +import io.element.android.libraries.push.impl.notifications.GroupedNotificationEvents +import io.element.android.libraries.push.impl.notifications.NotificationFactory +import io.element.android.libraries.push.impl.notifications.OneShotNotification +import io.element.android.libraries.push.impl.notifications.RoomNotification +import io.element.android.libraries.push.impl.notifications.SummaryNotification import io.mockk.every import io.mockk.mockk diff --git a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt index 6afab893c3d..20e872b8031 100644 --- a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt +++ b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt @@ -16,7 +16,11 @@ package io.element.android.services.appnavstate.test -import io.element.android.libraries.matrix.api.core.* +import io.element.android.libraries.matrix.api.core.MAIN_SPACE +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.SpaceId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.services.appnavstate.api.AppNavigationState fun anAppNavigationState( From a1390b7207423dcff5adb852558599e3c5a80023 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 09:24:55 +0200 Subject: [PATCH 102/104] Update danger/danger-js action to v11.2.5 (#309) Co-authored-by: renovate[bot] <29139614+renovate[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 4c00894a36a..cb4dbe5766e 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.2.4 + uses: danger/danger-js@11.2.5 with: args: "--dangerfile ./tools/danger/dangerfile.js" env: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 8f6fe6112ce..88ef78f9a84 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -42,7 +42,7 @@ jobs: yarn add danger-plugin-lint-report --dev - name: Danger lint if: always() - uses: danger/danger-js@11.2.4 + uses: danger/danger-js@11.2.5 with: args: "--dangerfile ./tools/danger/dangerfile-lint.js" env: From fc67b5f7b8f2e0f4c6644c230368f5ba5bb616ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 09:45:58 +0000 Subject: [PATCH 103/104] Sync Strings from Localazy (#307) Co-authored-by: bmarty --- .../createroom/impl/src/main/res/values/localazy.xml | 9 +++++++++ .../impl/src/main/res/values-fr/translations.xml | 5 +++++ .../src/main/res/values-fr/translations.xml | 11 +++++++++++ libraries/ui-strings/src/main/res/values/localazy.xml | 7 +++++++ 4 files changed, 32 insertions(+) create mode 100644 features/login/impl/src/main/res/values-fr/translations.xml diff --git a/features/createroom/impl/src/main/res/values/localazy.xml b/features/createroom/impl/src/main/res/values/localazy.xml index 8dbe7b43b15..48f055e0826 100644 --- a/features/createroom/impl/src/main/res/values/localazy.xml +++ b/features/createroom/impl/src/main/res/values/localazy.xml @@ -3,4 +3,13 @@ "New room" "Invite people" "Add people" + "Messages in this room are encrypted. Encryption can’t be disabled afterwards." + "Private room (invite only)" + "Messages are not encrypted and anyone can read them. You can enable encryption at a later date." + "Public room (anyone)" + "Room name" + "e.g. Product Sprint" + "Create a room" + "Topic (optional)" + "What is this room about?" \ No newline at end of file diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 00000000000..1b64d9f5fd7 --- /dev/null +++ b/features/login/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,5 @@ + + + "Continuer" + "Continuer" + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 5bdce47d20b..de1a80a9f54 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -1,5 +1,16 @@ + "Masquer le mot de passe" + "Envoyer des fichiers" + "Afficher le mot de passe" + "Menu utilisateur" + "Retour" + "Annuler" + "Effacer" + "Fermer" "Confirmer" + "Continuer" + "Copier" + "Copier le lien" "fr" \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index c37935d35bb..37665e72074 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -4,8 +4,10 @@ "Send files" "Show password" "User menu" + "Accept" "Back" "Cancel" + "Choose photo" "Clear" "Close" "Complete verification" @@ -13,13 +15,16 @@ "Continue" "Copy" "Copy link" + "Create" "Create a room" + "Decline" "Disable" "Done" "Edit" "Enable" "Invite" "Invite friends to %1$s" + "Invites" "Learn more" "Leave" "Leave room" @@ -45,6 +50,7 @@ "Start" "Start chat" "Start verification" + "Take photo" "View Source" "Yes" "About" @@ -131,6 +137,7 @@ "This is the beginning of %1$s." "This is the beginning of this conversation." "New" + "%1$s invited you" "Block user" "Check if you want to hide all current and future messages from this user" "Block" From 775cf2afdb4d730ba4e5a5b1148351529dda93cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Apr 2023 10:34:51 +0200 Subject: [PATCH 104/104] Update dependency com.bumble.appyx:core to v1.2.0 (#314) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f81ce0e8353..2f39ffda8bc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,7 +37,7 @@ datetime = "0.4.0" serialization_json = "1.5.0" showkase = "1.0.0-beta17" jsoup = "1.15.4" -appyx = "1.1.2" +appyx = "1.2.0" dependencycheck = "8.2.1" stem = "2.3.0" sqldelight = "1.5.5"