From 8ce6af7951d28f39dc0e913b8bb9f4063c089fff Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 13 Mar 2023 14:37:25 +0100 Subject: [PATCH 001/119] 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 1e3de32f22..316f8ab507 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 d228aa2acb..38a11235df 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 bec72cbc22722246f3b8d20b5ed4a7be69ddc5d5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 13 Mar 2023 15:17:44 +0100 Subject: [PATCH 002/119] 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 316f8ab507..fdf0fcf0a3 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 80f67bab6b..8cce1df9e6 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 6c77f11ac2..d4324432f3 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 08fb6c0a90af3a8ee388f549e58b379940760710 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 13 Mar 2023 15:21:03 +0100 Subject: [PATCH 003/119] 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 0000000000..d9aa72f7ba --- /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 09bfee08f7..31b022b3f2 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 0000000000..16fd1e855c --- /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 aa2bb224e57a78c1fb80e49efc0a5657614d5493 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 14 Mar 2023 09:30:00 +0100 Subject: [PATCH 004/119] 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 9b07f37daf..5b43b00922 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 64f4740029d818bfd99530ef197e88d6f0ba7458 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 14 Mar 2023 14:57:14 +0100 Subject: [PATCH 005/119] 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 fdf0fcf0a3..0b8833efbb 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 4a9ee85fc8..d12d139a73 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 8cce1df9e6..e2fc6db401 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 0000000000..f5305f006b --- /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 0000000000..2a4f9b8ac1 --- /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 0000000000..27a6827364 --- /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 0000000000..1d6f459d91 --- /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 0000000000..3ed22d7dae --- /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 0000000000..3fb4841aba --- /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 0000000000..823ea0c88c --- /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 0000000000..11950ad5f1 --- /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 0000000000..1d6f459d91 --- /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 0000000000..cc2b9100ec --- /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 0000000000..cf7a5f377b --- /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 0000000000..fa5e6a0e5d --- /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 0000000000..9b8b6c2281 --- /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 0000000000..3d602aeb9b --- /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 0000000000..42993828a9 --- /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 0000000000..d351067e52 --- /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 0000000000..2e87f360a5 --- /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 0000000000..e9f8cb985f --- /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 0000000000..368f2e2336 --- /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 0000000000..5e2e33d7d3 --- /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 0000000000..34c78a237e --- /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 0000000000..09dbf28a33 --- /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 0000000000..9fad5a37bc --- /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 0000000000..0fcdbafb69 --- /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 0000000000..d2d1c96506 --- /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 0000000000..1de015b770 --- /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 0000000000..1a70d94ee4 --- /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 0000000000..9a91f1f1ac --- /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 0000000000..0e37c14e12 --- /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 0000000000..c4227b3db2 --- /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 0000000000..6e92c2ec60 --- /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 0000000000..91b62eba0e --- /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 0000000000..778fe20d7b --- /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 0000000000..31ec28d023 --- /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 0000000000..b2a7129998 --- /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 0000000000..d0ab023789 --- /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 0000000000..ea46654d88 --- /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 0000000000..ae936e693b --- /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 0000000000..7f9ec73343 --- /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 0000000000..c40680a1fd --- /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 0000000000..c8ba481323 --- /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 0000000000..7766ed04e8 --- /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 0000000000..f935a36366 --- /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 0000000000..8b5fa70365 --- /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 0000000000..808bf4114b --- /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 0000000000..fe2b9ccfd8 --- /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 0000000000..1d82fc31e4 --- /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 0000000000..2e91ca3467 --- /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 0000000000..b09482264b --- /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 0000000000..34e8da9723 --- /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 0000000000..864fd6c42b --- /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 0000000000..42c0fe61af --- /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 0000000000..3c49ba742f --- /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 0000000000..bcbf614659 --- /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 0000000000..52f3ad3be6 --- /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 0000000000..e1d5c3347b --- /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 0000000000..7413264d5d --- /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 0000000000..8474682ab6 --- /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 0000000000..e9b119c969 --- /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 0000000000..6e04238a1a --- /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 0000000000..ce2fee2015 --- /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 944b17ab52..0c0e3ec2cf 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 bc7fb0a2bbcdb4de71cfe140319c62af77a39c0d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Mar 2023 10:05:36 +0100 Subject: [PATCH 006/119] 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 3ed22d7dae..335ed9426c 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 0000000000..9e8acc4d8f --- /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 11950ad5f1..d98be34759 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 cf7a5f377b..bf3a252315 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 2e87f360a5..c75cc6f76b 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 0000000000..02bd7850e9 --- /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 0000000000..5cd46f873d --- /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 0000000000..7adedfcfd2 --- /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 0000000000..b7649f6800 --- /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 0000000000..ce41d2d83e --- /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 0000000000..37e97a238c --- /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 0000000000..13d9cbad1d --- /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 7ad385965e0277d3eda9df67e91c65b4f101ecd2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Mar 2023 14:39:00 +0100 Subject: [PATCH 007/119] 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 c75cc6f76b..e8058eab3f 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 09dbf28a33..e9bccf7cdd 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 9fad5a37bc..6a73cedf92 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 9a91f1f1ac..75bed1027b 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 cb0cc70102a18f4eecc02841e592a9eb95430d38 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 17 Mar 2023 13:28:57 +0100 Subject: [PATCH 008/119] 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 d98be34759..f9a265bcf7 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 d7fd3e3b2279363b77ba8d2cdc16e37bc223cc1e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 20 Mar 2023 14:57:35 +0100 Subject: [PATCH 009/119] 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 1d6f459d91..0000000000 --- a/libraries/push/api/src/main/AndroidManifest.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 42dfaca929812c3010e5debf0fa0453d0c4cc01f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 20 Mar 2023 15:50:14 +0100 Subject: [PATCH 010/119] 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 0000000000..38f2e2227c --- /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 0000000000..d4ac1ec739 --- /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 c133caf4428b77596cbbe18f346f8a2f3bef3870 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 20 Mar 2023 16:29:33 +0100 Subject: [PATCH 011/119] 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 e8058eab3f..5525b04d4c 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 368f2e2336..958cd474be 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 778fe20d7b..d9c3a8a167 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 ea46654d88..21a006e3f4 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 fe2b9ccfd8..3c5110b67a 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 34e8da9723..2a2311e591 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 864fd6c42b..13598f343a 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 500d4801a7c74fa81cb2a9b1568cd875ffb0cc7f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 21 Mar 2023 18:48:38 +0100 Subject: [PATCH 012/119] 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 1cfcc40510..b2d2b391d8 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 e865527b80..436961141e 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 ac37dfe30d..e77480f7c0 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 a14ef74e94..2deb13ff2b 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 d1ca647d62..62fd918cca 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 1f49d8db0f..9567ef9464 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 800da0d5b3..8f01f28545 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 0000000000..d86f790a44 --- /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 0000000000..5267520320 --- /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 0000000000..98519411bd --- /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 0000000000..975674820b --- /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 0000000000..440b7e3e72 --- /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 0000000000..5cf74aca90 --- /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 0000000000..b35ce36380 --- /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 0000000000..43608d36e8 --- /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 0000000000..db56946922 --- /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 0000000000..9ee29b7a61 --- /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 0000000000..25b41e2a71 --- /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 0000000000..ba4c59b2e7 --- /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 0000000000..08a3f76130 --- /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 f9a265bcf7..bb1c5bfa76 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 0000000000..e1fd17332e --- /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 314421ebc8..9f0cf92099 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 dfc759685d15cd3228afdad8728aa4109b812ad1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 22 Mar 2023 10:10:19 +0100 Subject: [PATCH 013/119] 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 5267520320..a0b2411459 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 440b7e3e72..382d8e9653 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 0000000000..15acd868f2 --- /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 db56946922..12eb5b81fd 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 ba4c59b2e7..be326a71ed 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,35 @@ 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) @@ -40,6 +70,117 @@ class DefaultPermissionsPresenterTest { }.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) + presenter.present() + }.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 0000000000..2c67061811 --- /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 08a3f76130..3f5d925ccd 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 b911229f53b3166aea7cf1071c786dfbe8db69db Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 22 Mar 2023 11:38:46 +0100 Subject: [PATCH 014/119] 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 436961141e..79db4d5360 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 3f3e43e2e7..dc16984e53 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 0000000000..7319f73104 --- /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 0000000000..293edb2cac --- /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 9ad0df0c0c..8ad1c125d7 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 5dce2feafe..7d10663027 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 581ec6a1afcf4222296e79c55fbaf32126c04629 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 22 Mar 2023 12:04:22 +0100 Subject: [PATCH 015/119] 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 79db4d5360..b679c2d823 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 e77480f7c0..c38120161b 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 dc16984e53..18ead8f3e6 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 98519411bd..c4ab065ca0 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 12eb5b81fd..c09a595783 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 be326a71ed..10e6edf3f5 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 293edb2cac..653fe49268 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 0000000000..b982969483 --- /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 7d10663027..9d1c7a495d 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 8a100500f03b2679b90aa0f5ec8b6a7da3138ca7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Mar 2023 16:19:22 +0200 Subject: [PATCH 016/119] 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 3d602aeb9b..16ce8d73ab 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 958cd474be..a6b50a58dd 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 3c5110b67a..bfa80908bf 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 2a2311e591..360c5c1bb5 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 13598f343a..86b741bbd7 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 0000000000..b560669f57 --- /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 da4b49ce173fc5d9376ecba44ddb77d939be6875 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Mar 2023 12:03:17 +0200 Subject: [PATCH 017/119] 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 6a73cedf92..3e0d0a3d6d 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 0000000000..0db59e42f7 --- /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 0000000000..4ab6c775e3 --- /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 0000000000..8a23409558 --- /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 0000000000..96f4ee25fd --- /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 0000000000..f283ab4607 --- /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 0000000000..b3befee36b --- /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 0000000000..25823a57e8 --- /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 0000000000..0bc826398a --- /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 0000000000..1a6d52e660 --- /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 80fdd5f1273609cbc8c0aeb474413bbf8da05a65 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Mar 2023 15:25:07 +0200 Subject: [PATCH 018/119] 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 13ebf1c0e8..aff83c3b5a 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 b2ce80da69ced0bd6c19dba5c55b6dbede00cbf9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Mar 2023 15:40:34 +0200 Subject: [PATCH 019/119] 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 de0ec2f727..1637bd809f 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 b73ffdeb9a..ce5b6e24f2 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 6c32bcd1f3..15c3024712 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 d8fb15338c..ea8471a36a 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 885c04af78..0260604f6e 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 be8ce499d0338b9b29fe4c0a0d8ba4e91f652d9c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Mar 2023 16:00:54 +0200 Subject: [PATCH 020/119] 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 8a991771a5..9c0d0c17ac 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 0000000000..a868aeab85 --- /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 0000000000..43a90f5be2 --- /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 2fe34bfa55..4acafa321e 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 0000000000..39dd5c1d11 --- /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 16ce8d73ab..b8dc1ad384 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 5525b04d4c..20cbd078e2 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 e9bccf7cdd..81afe726f2 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 0fcdbafb69..49a63ba4aa 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 8a23409558..1d7a1e6247 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 1f09f5f0eb67ba7d78bb35ec07f6e28b95408e21 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Mar 2023 18:04:32 +0200 Subject: [PATCH 021/119] 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 1028c6f16d..42d82913b8 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 9c0d0c17ac..82c7074729 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 0000000000..27fc15c2c6 --- /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 0000000000..2c1672d864 --- /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 4acafa321e..0fa5a668c8 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 0000000000..ae47beb700 --- /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 0000000000..27091c17ee --- /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 27a6827364..be1bbc13ef 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 335ed9426c..77adb869c5 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 bf3a252315..e9b7efcdee 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 20cbd078e2..8a713585b6 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 3e0d0a3d6d..f6a4b5d83d 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 75bed1027b..06445d7ca6 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 0e37c14e12..fbde04cc36 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 c4227b3db2..fc4ed55783 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 7413264d5d..1504e6ec00 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 afbd4672e6381680f215de1d67bb1a4e7de274c8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Mar 2023 14:18:23 +0200 Subject: [PATCH 022/119] 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 | 14 ++++++-- .../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, 124 insertions(+), 16 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 0000000000..f7b96f3aad --- /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 8a713585b6..b4952f35b4 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 f6a4b5d83d..0357e40a0a 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 0000000000..936ccfcbde --- /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 7f9ec73343..838356b370 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 -class NotificationDisplayer @Inject constructor( - @ApplicationContext context: Context, -) { +const val TEMPORARY_ID = 101 +class NotificationDisplayer @Inject constructor( + @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 c40680a1fd..1bf3e68f7f 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 f935a36366..5a3f008963 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 8b5fa70365..e0fc44cca0 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 bfa80908bf..3fce6e0d84 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 b560669f57..e7b8618cc2 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 281eb617b58750988311411c4de02b6dbc4c67d8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Mar 2023 14:47:49 +0200 Subject: [PATCH 023/119] 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 998c7cdea4..b44bbc9af6 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 0000000000..a788e56f19 --- /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 0000000000..b9f4580ee8 --- /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 c1f80cda80b5ca7b5e7caf6697b7db3e81c5ca0b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Mar 2023 15:05:08 +0200 Subject: [PATCH 024/119] 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 b2d2b391d8..8ece3e5841 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 42d82913b8..b20dcc8f44 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 0000000000..2712b42003 --- /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 0000000000..62127a5ea7 --- /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 0000000000..a5c43801bd --- /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 0000000000..90ff2136e5 --- /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 0000000000..8071d7ca99 --- /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 b679c2d823..e5c0e289e0 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 c38120161b..ac37dfe30d 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 2deb13ff2b..a14ef74e94 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 62fd918cca..07e2fcff1a 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 9567ef9464..b4c7676daf 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 18ead8f3e6..3f3e43e2e7 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 9d1c7a495d..5dce2feafe 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 eaff43de3a146f461f5dabe51adcd9bd098335eb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Mar 2023 10:15:51 +0200 Subject: [PATCH 025/119] 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 b20dcc8f44..caef28bb9e 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 0000000000..6950b9b699 --- /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 62127a5ea7..a845ec4600 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 8071d7ca99..5db19ccae7 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 77adb869c5..7d0f2cf4cb 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 e9b7efcdee..82c7062959 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 b4952f35b4..710688e520 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 961d0ecdbc46acfdc125475654c42e58f61b3a47 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Mar 2023 12:04:50 +0200 Subject: [PATCH 026/119] 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 a868aeab85..ef2291f8ce 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 0fa5a668c8..bb4ec1c971 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 39dd5c1d11..4eaafef12d 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 b9f4580ee8..77087d132f 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 b9276aa60b3bf675dfc14f36eaf327490fca64c9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Mar 2023 15:55:14 +0200 Subject: [PATCH 027/119] 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 a845ec4600..82f927a743 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 7d0f2cf4cb..5582e7fe92 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 1d6f459d91..71fc629aaa 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 cc2b9100ec..0000000000 --- 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 82c7062959..327a3eea96 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 710688e520..8455624585 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 a6b50a58dd..f77b5b2833 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 fa5e6a0e5d..9e9b28ecb8 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 0000000000..906816eb56 --- /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 fbde04cc36..af82ebce74 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 81afe726f2..f0e3bc1609 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 1de015b770..aef87e7df3 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 0000000000..359779fd8a --- /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 91b62eba0e..209c42f4ae 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 1504e6ec00..0000000000 --- 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 06445d7ca6..0955c864cd 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 0357e40a0a..bab955b419 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 42993828a9..4c93b8a929 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 d351067e52..de66ed3914 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 fc4ed55783..3e6a8199ec 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 e9f8cb985f..50ca94f30d 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 0000000000..9788ecf1a1 --- /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 34c78a237e..6cd1af1de3 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 49a63ba4aa..db4489a489 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 1a70d94ee4..90857d990d 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 0000000000..a66b283519 --- /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 0000000000..6f25599e54 --- /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 0000000000..4b16a21491 --- /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 1637bd809f..223ab16aa5 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 dae0cae8bb55e6b82e144391120e6913ab048f72 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Mar 2023 15:59:23 +0200 Subject: [PATCH 028/119] 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 8455624585..8407360385 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 bab955b419..2f7a1947c5 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 62db96476d111352a2f08550a5000bdac3030f96 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Mar 2023 16:02:05 +0200 Subject: [PATCH 029/119] 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 2c1672d864..9dec0821a3 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 bb4ec1c971..cd14b35fbc 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 27091c17ee..1197021161 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 a788e56f19..879a9694a3 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 c52ad084e949c74192501a1ba22ff58d781b7984 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Mar 2023 22:17:19 +0200 Subject: [PATCH 030/119] 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 4b16a21491..0323713de7 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 223ab16aa5..d79d700030 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 0000000000..7bcb4db792 --- /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 0000000000..e61b4e2bba --- /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 ce5b6e24f2..e23e34983c 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 15c3024712..9394b66e66 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 0000000000..dbcfca2d4c --- /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 41e0249fbf7fee483d28ade321c83d6c73c9cfdd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Apr 2023 09:42:16 +0200 Subject: [PATCH 031/119] 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 f0e3bc1609..8769baa947 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 2f7a1947c5..280fda99e5 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 db4489a489..81dd389e78 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 a2dc4db6849b4a57e7d02214f30c5eb261062df4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Apr 2023 11:19:29 +0200 Subject: [PATCH 032/119] 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 dbcfca2d4c..8fa5d9dd16 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 6f6a1e27fe97a6d1647fce59399242414e50463c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Apr 2023 11:54:40 +0200 Subject: [PATCH 033/119] 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 31033b0500..828788ed80 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 48044a3cc5e8f3158e890fe085e1cb18879e050f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 14:50:06 +0200 Subject: [PATCH 043/119] 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 adabca5b18..12ed3f1993 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 0000000000..3a11adb5d3 --- /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 e0833bd2dc..0000000000 --- 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 28cef98c60..c37935d35b 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 155a42836a..59c6bd6911 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 78b8d5cf6ef5c81398e2acded119878f95823a03 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 15:13:34 +0200 Subject: [PATCH 044/119] 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 dcdb800a19..0000000000 --- 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 28a98f496f..eeb02ebb8b 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 84e4e0f1a9cc9339916c1c4fa35e0b3f9c60bcd7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 15:20:56 +0200 Subject: [PATCH 045/119] 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 7319f73104..8a1949814f 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 b982969483..0000000000 --- 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 0000000000..36b908cb0d --- /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 2ac74ba40facdc72c3549752dc2969e226bc1820 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 15:28:05 +0200 Subject: [PATCH 046/119] 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 2712b42003..664ec1f663 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 82f927a743..b628f19bd1 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 a5c43801bd..8cf8060981 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 90ff2136e5..b131d6d610 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 c09a595783..c422cd1ec9 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 c014d4e12fc8cdb5bd5a33ea489e81b738147af6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 15:44:11 +0200 Subject: [PATCH 047/119] 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 0000000000..64767eaafc --- /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 fad849ce2150f39d3e8d45b263bc30805ea6d374 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 15:50:04 +0200 Subject: [PATCH 048/119] 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 1c380c260a..a386a89caf 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 df492cfd0ec0165916ff016179f812cf8f30f320 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 15:55:50 +0200 Subject: [PATCH 049/119] 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 8f01f28545..5f18f46827 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 c422cd1ec9..9e91869549 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 b56ab86ef1c389020fba60d018b17930cc6f1259 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Apr 2023 18:14:32 +0200 Subject: [PATCH 050/119] 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 2c67061811..c204ff5fc6 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 efe3d3add4a848a954b59ad9b276072d4b342d11 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 6 Apr 2023 09:12:44 +0200 Subject: [PATCH 051/119] 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 38a11235df..7477d4da73 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 ca54e11b97bb1d1c78432a0df4ffa1c20608381a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 10:38:47 +0200 Subject: [PATCH 052/119] 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 43608d36e8..84cd531528 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 6af4057e746b7e498e248072806f108a5ba2fccb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 12:26:25 +0200 Subject: [PATCH 053/119] 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 e3c745b8f6..afb5fe9f75 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 32be8f29e61fc330bc444c50e215273acceadf9b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 12:29:35 +0200 Subject: [PATCH 054/119] 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 afb5fe9f75..f171a506f1 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 9cca2405e8dc4d420ea00423c99101217249dffc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 14:39:13 +0200 Subject: [PATCH 055/119] 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 5892a87c38..0f35f9ec59 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 67955988cf6e8866499d738f4841e9e1bd7e8b43 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:01:34 +0200 Subject: [PATCH 056/119] 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 f171a506f1..2b44c60352 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 ea9f9ccdc3b66cf46484a93a3a22a912171c64e5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:03:02 +0200 Subject: [PATCH 057/119] 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 2b44c60352..e5c3733872 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 ad0886391b56088d87b509c5052a497d316e127e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 12:29:35 +0200 Subject: [PATCH 058/119] Revert "Run every 10 minutes to check the script. (to be reverted!)" This reverts commit 32be8f29e61fc330bc444c50e215273acceadf9b. --- .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 e5c3733872..02f93cc3e1 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 30a0da74f045d36505bbf2cf8bf3275918e06ff7 Mon Sep 17 00:00:00 2001 From: Hans Christian Schmitz Date: Mon, 10 Apr 2023 08:21:57 +0200 Subject: [PATCH 059/119] 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 290aace3d5..b7491f5b3b 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 4884217b0c..b2abb1a373 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 f06bf83b20aab27c7a09cb7cf83c592e2bb4c5fb 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 060/119] 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 e8b6ab285c..d35051d2cc 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 b9007259193f04d694377b88ba9832164109fdaa Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 10 Apr 2023 09:00:02 +0200 Subject: [PATCH 061/119] 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 0ead18c75e..6c73607196 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 f8f445a6df..f1ac346909 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 5e2e33d7d3..226d0c5669 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 98b64d4e0b..055de6fc47 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 6e92c2ec60..a24f088998 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 7c083555e4..1b1fe2723e 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 6463773e7b..5ae39e1102 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 31ec28d023..b3f0b1e0f2 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 d0ab023789..7bd76f9f42 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 078de3a8a8..a9c7036418 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 4688a4a24c..8d3bfda4c3 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 521f4f3c8c..277dc3b822 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 eeb02ebb8b..add8fd74eb 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 1d82fc31e4..5b15dc78d2 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 b2a5fcbedb..734c34b051 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 40d84496a5..b1bb7cd032 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 31d3eb7ea0..864155e522 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 8474682ab6..ffbd575aa4 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 4c93b8a929..08bd4a8326 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 a66b283519..82c4beaf20 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 d9ba17c28e..7d7812e6cb 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 6afab893c3..20e872b803 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 1ba60543015044c699a112fedc4992f601f4403a 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 062/119] 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 4c00894a36..cb4dbe5766 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 8f6fe6112c..88ef78f9a8 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 6554dec73cf7dc74c93c8dff87eb690f0ba5805f 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 063/119] 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 8dbe7b43b1..48f055e082 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 0000000000..1b64d9f5fd --- /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 5bdce47d20..de1a80a9f5 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 c37935d35b..37665e7207 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 0550a32821aa056d68db77939a86aa302e5b1f0b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 6 Apr 2023 16:58:29 +0200 Subject: [PATCH 064/119] Add test for Push parsers. --- .../android/libraries/matrix/test/TestData.kt | 6 +- .../impl/unifiedpush/PushDataUnifiedPush.kt | 10 +- .../impl/unifiedpush/UnifiedPushParser.kt | 4 +- .../impl/firebase/FirebasePushParserTest.kt | 100 ++++++++++++++++++ .../impl/unifiedpush/UnifiedPushParserTest.kt | 95 +++++++++++++++++ 5 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index ec749025dc..6b4d83e43b 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -31,9 +31,9 @@ val A_USER_ID = UserId("@alice:server.org") val A_USER_ID_2 = UserId("@bob:server.org") val A_SESSION_ID = SessionId(A_USER_ID.value) val A_SESSION_ID_2 = SessionId(A_USER_ID_2.value) -val A_SPACE_ID = SpaceId("!aSpaceId") -val A_ROOM_ID = RoomId("!aRoomId") -val A_ROOM_ID_2 = RoomId("!aRoomId2") +val A_SPACE_ID = SpaceId("!aSpaceId:domain") +val A_ROOM_ID = RoomId("!aRoomId:domain") +val A_ROOM_ID_2 = RoomId("!aRoomId2:domain") val A_THREAD_ID = ThreadId("\$aThreadId") val AN_EVENT_ID = EventId("\$anEventId") val AN_EVENT_ID_2 = EventId("\$anEventId2") diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt index 56513ab970..0bc6a6bce5 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt @@ -41,19 +41,19 @@ import kotlinx.serialization.Serializable */ @Serializable data class PushDataUnifiedPush( - val notification: PushDataUnifiedPushNotification? + val notification: PushDataUnifiedPushNotification? = null ) @Serializable data class PushDataUnifiedPushNotification( - @SerialName("event_id") val eventId: String?, - @SerialName("room_id") val roomId: String?, - @SerialName("counts") var counts: PushDataUnifiedPushCounts?, + @SerialName("event_id") val eventId: String? = null, + @SerialName("room_id") val roomId: String? = null, + @SerialName("counts") var counts: PushDataUnifiedPushCounts? = null, ) @Serializable data class PushDataUnifiedPushCounts( - @SerialName("unread") val unread: Int? + @SerialName("unread") val unread: Int? = null ) fun PushDataUnifiedPush.toPushData() = PushData( 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 index 9788ecf1a1..05cd8425b7 100644 --- 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 @@ -23,7 +23,9 @@ import kotlinx.serialization.json.Json import javax.inject.Inject class UnifiedPushParser @Inject constructor() { + private val json by lazy { Json { ignoreUnknownKeys = true } } + fun parse(message: ByteArray): PushData? { - return tryOrNull { Json.decodeFromString(String(message)) }?.toPushData() + return tryOrNull { json.decodeFromString(String(message)) }?.toPushData() } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt new file mode 100644 index 0000000000..9d02913cf8 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt @@ -0,0 +1,100 @@ +/* + * 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.firebase + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.push.impl.push.PushData +import org.junit.Test + +class FirebasePushParserTest { + private val validData = PushData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + unread = 1, + clientSecret = "a-secret" + ) + + private val emptyData = PushData( + eventId = null, + roomId = null, + unread = null, + clientSecret = null + ) + + @Test + fun `test edge cases Firebase`() { + val pushParser = FirebasePushParser() + // Empty Json + assertThat(pushParser.parse(emptyMap())).isEqualTo(emptyData) + // Bad Json + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("unread", "str"))).isEqualTo(validData.copy(unread = null)) + // Extra data + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("extra", "5"))).isEqualTo(validData) + } + + @Test + fun `test Firebase format`() { + val pushParser = FirebasePushParser() + assertThat(pushParser.parse(FIREBASE_PUSH_DATA)).isEqualTo(validData) + } + + @Test + fun `test empty roomId`() { + val pushParser = FirebasePushParser() + val expected = validData.copy(roomId = null) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", null))).isEqualTo(expected) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", ""))).isEqualTo(expected) + } + + @Test + fun `test invalid roomId`() { + val pushParser = FirebasePushParser() + val expected = validData.copy(roomId = null) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain"))).isEqualTo(expected) + } + + @Test + fun `test empty eventId`() { + val pushParser = FirebasePushParser() + val expected = validData.copy(eventId = null) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", null))).isEqualTo(expected) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", ""))).isEqualTo(expected) + } + + @Test + fun `test invalid eventId`() { + val pushParser = FirebasePushParser() + val expected = validData.copy(eventId = null) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId"))).isEqualTo(expected) + } + + companion object { + private val FIREBASE_PUSH_DATA = mapOf( + "event_id" to AN_EVENT_ID.value, + "room_id" to A_ROOM_ID.value, + "unread" to "1", + "prio" to "high", + "cs" to "a-secret", + ) + } +} + +private fun Map.mutate(key: String, value: String?): Map { + return toMutableMap().apply { put(key, value) } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt new file mode 100644 index 0000000000..00c92bb08a --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.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.push.impl.unifiedpush + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.push.impl.push.PushData +import org.junit.Test + +class UnifiedPushParserTest { + private val validData = PushData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + unread = 1, + // TODO handle client secret here. + clientSecret = null + ) + + private val emptyData = PushData( + eventId = null, + roomId = null, + unread = null, + clientSecret = null + ) + + @Test + fun `test edge cases UnifiedPush`() { + val pushParser = UnifiedPushParser() + // Empty string + assertThat(pushParser.parse("".toByteArray())).isNull() + // Empty Json + assertThat(pushParser.parse("{}".toByteArray())).isEqualTo(emptyData) + // Bad Json + assertThat(pushParser.parse("ABC".toByteArray())).isNull() + } + + @Test + fun `test UnifiedPush format`() { + val pushParser = UnifiedPushParser() + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.toByteArray())).isEqualTo(validData) + } + + @Test + fun `test empty roomId`() { + val pushParser = UnifiedPushParser() + val expected = validData.copy(roomId = null) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray())).isEqualTo(expected) + } + + @Test + fun `test invalid roomId`() { + val pushParser = UnifiedPushParser() + val expected = validData.copy(roomId = null) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"))).isEqualTo(expected) + } + + @Test + fun `test empty eventId`() { + val pushParser = UnifiedPushParser() + val expected = validData.copy(eventId = null) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""))).isEqualTo(expected) + } + + @Test + fun `test invalid eventId`() { + val pushParser = UnifiedPushParser() + val expected = validData.copy(eventId = null) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"))).isEqualTo(expected) + } + + companion object { + private val UNIFIED_PUSH_DATA = + "{\"notification\":{\"event_id\":\"${AN_EVENT_ID.value}\",\"room_id\":\"${A_ROOM_ID.value}\",\"counts\":{\"unread\":1},\"prio\":\"high\"}}" + // TODO Check client secret format? + } +} + +private fun String.mutate(oldValue: String, newValue: String): ByteArray { + return replace(oldValue, newValue).toByteArray() +} From 2926b7443d983f078abe88c16b2558aab2fd5108 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:12:29 +0200 Subject: [PATCH 065/119] Cleanup rule, TemplatePresenter does not exist anymore --- build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index d7e3a12fcc..66c842630c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -230,7 +230,6 @@ koverMerged { target = kotlinx.kover.api.VerificationTarget.CLASS overrideClassFilter { includes += "*Presenter" - excludes += "*TemplatePresenter" excludes += "*Fake*Presenter" excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*" } From 3b49ab0e7ab2dd0116f0d53902b942cef23b37f5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:13:06 +0200 Subject: [PATCH 066/119] Add default value for `noActivityFoundMessage` --- .../element/android/appnav/loggedin/LoggedInView.kt | 2 +- .../features/roomdetails/impl/RoomDetailsNode.kt | 2 -- .../libraries/androidutils/system/SystemUtils.kt | 11 ++++++----- .../androidutils/src/main/res/values/localazy.xml | 4 ++++ .../src/main/res/values-es/translations.xml | 3 +-- .../src/main/res/values-it/translations.xml | 3 +-- .../src/main/res/values-ro/translations.xml | 3 +-- libraries/ui-strings/src/main/res/values/localazy.xml | 1 - tools/localazy/config.json | 6 ++++++ 9 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 libraries/androidutils/src/main/res/values/localazy.xml 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 5db19ccae7..3f415c3dc1 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 @@ -38,7 +38,7 @@ fun LoggedInView( state = state.permissionsState, modifier = modifier, openSystemSettings = { - activity?.let { openAppSettingsPage(it, "") } + activity?.let { openAppSettingsPage(it) } } ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 937d755df5..f0e1f28cf3 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -31,7 +31,6 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.ui.strings.R as StringR @ContributesNode(RoomScope::class) class RoomDetailsNode @AssistedInject constructor( @@ -57,7 +56,6 @@ class RoomDetailsNode @AssistedInject constructor( activityResultLauncher = null, chooserTitle = context.getString(R.string.screen_room_details_share_room_title), text = permalink, - noActivityFoundMessage = context.getString(StringR.string.error_no_compatible_app_found) ) } } 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 5f18f46827..c5bccc97f5 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 @@ -32,6 +32,7 @@ import androidx.activity.result.ActivityResultLauncher import androidx.annotation.ChecksSdkIntAtLeast import androidx.annotation.RequiresApi import androidx.core.content.getSystemService +import io.element.android.libraries.androidutils.R import io.element.android.libraries.androidutils.compat.getApplicationInfoCompat /** @@ -125,7 +126,7 @@ fun startNotificationSettingsIntent(context: Context, activityResultLauncher: Ac fun openAppSettingsPage( activity: Activity, - noActivityFoundMessage: String, + noActivityFoundMessage: String = activity.getString(R.string.error_no_compatible_app_found), ) { try { activity.startActivity( @@ -156,7 +157,7 @@ fun startNotificationChannelSettingsIntent(activity: Activity, channelID: String fun startAddGoogleAccountIntent( context: Context, activityResultLauncher: ActivityResultLauncher, - noActivityFoundMessage: String, + noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found), ) { val intent = Intent(Settings.ACTION_ADD_ACCOUNT) intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf("com.google")) @@ -171,7 +172,7 @@ fun startAddGoogleAccountIntent( fun startInstallFromSourceIntent( context: Context, activityResultLauncher: ActivityResultLauncher, - noActivityFoundMessage: String, + noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found), ) { val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES) .setData(Uri.parse(String.format("package:%s", context.packageName))) @@ -189,7 +190,7 @@ fun startSharePlainTextIntent( text: String, subject: String? = null, extraTitle: String? = null, - noActivityFoundMessage: String, + noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found), ) { val share = Intent(Intent.ACTION_SEND) share.type = "text/plain" @@ -217,7 +218,7 @@ fun startSharePlainTextIntent( fun startImportTextFromFileIntent( context: Context, activityResultLauncher: ActivityResultLauncher, - noActivityFoundMessage: String, + noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found), ) { val intent = Intent(Intent.ACTION_GET_CONTENT).apply { type = "text/plain" diff --git a/libraries/androidutils/src/main/res/values/localazy.xml b/libraries/androidutils/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..0599c8922b --- /dev/null +++ b/libraries/androidutils/src/main/res/values/localazy.xml @@ -0,0 +1,4 @@ + + + "No compatible app was found to handle this action." + \ 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 index 4b14f3a4a7..58b25eaf3c 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -107,7 +107,6 @@ "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" @@ -145,4 +144,4 @@ "General" "Versión: %1$s (%2$s)" "es" - \ 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 index 96d0648d3b..2eb58f0d6e 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -107,7 +107,6 @@ "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" @@ -145,4 +144,4 @@ "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 1872bb057f..ba066efae9 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -107,7 +107,6 @@ "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" @@ -147,4 +146,4 @@ "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 37665e7207..de11a74eac 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -114,7 +114,6 @@ "Symbols" "Failed creating the permalink" "Failed loading messages" - "No compatible app was found to handle this action." "Some messages have not been sent" "Sorry, an error occurred" "Hey, talk to me on %1$s: %2$s" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 59c6bd6911..37f8c895f1 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -43,6 +43,12 @@ "rich_text_editor_.*" ] }, + { + "name": ":libraries:androidutils", + "includeRegex": [ + "error_no_compatible_app_found" + ] + }, { "name": ":libraries:push:impl", "includeRegex": [ From 40660ca317f0ef0048f1e48d766a8dcfbb2f3877 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:27:34 +0200 Subject: [PATCH 067/119] Add some check on as*Id() methods, to fail in debug mode. --- .../libraries/matrix/api/core/EventId.kt | 7 +++++- .../matrix/api/core/MatrixPatterns.kt | 24 +++++++++++++++++++ .../libraries/matrix/api/core/RoomId.kt | 7 +++++- .../libraries/matrix/api/core/SessionId.kt | 8 ++++++- .../libraries/matrix/api/core/SpaceId.kt | 7 +++++- .../libraries/matrix/api/core/ThreadId.kt | 7 +++++- .../libraries/matrix/api/core/UserId.kt | 7 +++++- 7 files changed, 61 insertions(+), 6 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt index dc5e7ab16a..ffd5bb8ea2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt @@ -16,9 +16,14 @@ package io.element.android.libraries.matrix.api.core +import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable @JvmInline value class EventId(val value: String) : Serializable -fun String.asEventId() = EventId(this) +fun String.asEventId() = EventId(this).also { + if (BuildConfig.DEBUG && !MatrixPatterns.isEventId(this)) { + error("`$this` is not a valid event Id") + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt index f295fe81b9..a30baadb55 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt @@ -91,6 +91,14 @@ object MatrixPatterns { PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER ) + /** + * Tells if a string is a valid session Id. This is an alias for [isUserId] + * + * @param str the string to test + * @return true if the string is a valid session id + */ + fun isSessionId(str: String?) = isUserId(str) + /** * Tells if a string is a valid user Id. * @@ -101,6 +109,14 @@ object MatrixPatterns { return str != null && str matches PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER } + /** + * Tells if a string is a valid space id. This is an alias for [isRoomId] + * + * @param str the string to test + * @return true if the string is a valid space Id + */ + fun isSpaceId(str: String?) = isRoomId(str) + /** * Tells if a string is a valid room id. * @@ -134,6 +150,14 @@ object MatrixPatterns { str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4) } + /** + * Tells if a string is a valid thread id. This is an alias for [isEventId]. + * + * @param str the string to test + * @return true if the string is a valid thread id. + */ + fun isThreadId(str: String?) = isEventId(str) + /** * Tells if a string is a valid group id. * diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt index df10038b05..f711723c3f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt @@ -16,9 +16,14 @@ package io.element.android.libraries.matrix.api.core +import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable @JvmInline value class RoomId(val value: String) : Serializable -fun String.asRoomId() = RoomId(this) +fun String.asRoomId() = RoomId(this).also { + if (BuildConfig.DEBUG && !MatrixPatterns.isRoomId(this)) { + error("`$this` is not a valid room Id") + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt index bea1f3c671..8591876b29 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt @@ -16,6 +16,12 @@ package io.element.android.libraries.matrix.api.core +import io.element.android.libraries.matrix.api.BuildConfig + typealias SessionId = UserId -fun String.asSessionId() = SessionId(this) +fun String.asSessionId() = SessionId(this).also { + if (BuildConfig.DEBUG && !MatrixPatterns.isSessionId(this)) { + error("`$this` is not a valid session Id") + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt index 849dd7d637..1b8b33426b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.api.core +import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable @JvmInline @@ -26,4 +27,8 @@ value class SpaceId(val value: String) : Serializable */ val MAIN_SPACE = SpaceId("!mainSpace") -fun String.asSpaceId() = SpaceId(this) +fun String.asSpaceId() = SpaceId(this).also { + if (BuildConfig.DEBUG && !MatrixPatterns.isSpaceId(this)) { + error("`$this` is not a valid space Id") + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt index 57fc187406..f95c33bad3 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt @@ -16,9 +16,14 @@ package io.element.android.libraries.matrix.api.core +import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable @JvmInline value class ThreadId(val value: String) : Serializable -fun String.asThreadId() = ThreadId(this) +fun String.asThreadId() = ThreadId(this).also { + if (BuildConfig.DEBUG && !MatrixPatterns.isThreadId(this)) { + error("`$this` is not a valid Thread Id") + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt index 216faade45..91f9c6f11c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt @@ -16,9 +16,14 @@ package io.element.android.libraries.matrix.api.core +import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable @JvmInline value class UserId(val value: String) : Serializable -fun String.asUserId() = UserId(this) +fun String.asUserId() = UserId(this).also { + if (BuildConfig.DEBUG && !MatrixPatterns.isUserId(this)) { + error("`$this` is not a valid user Id") + } +} From 68fb2bd2ebb4265a64d05d1a7dfdf28e95876cf0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:30:09 +0200 Subject: [PATCH 068/119] `if` -> `when` --- .../permissions/api/PermissionsView.kt | 86 ++++++++++--------- 1 file changed, 45 insertions(+), 41 deletions(-) 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 382d8e9653..30f19aa31c 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 @@ -32,48 +32,52 @@ fun PermissionsView( ) { 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.CloseDialog) - 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." + when { + state.permissionGranted -> { + // Notification Granted, nothing to do + } + 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.CloseDialog) + 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 = {} + ) } - ConfirmationDialog( - modifier = modifier, - title = "Notifications", - content = textToShow, - submitText = "Request permission", - onSubmitClicked = { - state.eventSink.invoke(PermissionsEvents.OpenSystemDialog) - }, - onCancelClicked = { - state.eventSink.invoke(PermissionsEvents.CloseDialog) - }, - onDismiss = {} - ) } } From 5fc7870e4746ef13a9cb6f6195af3300f4f3d952 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:34:51 +0200 Subject: [PATCH 069/119] Add more state for more previews. --- .../libraries/permissions/api/PermissionsViewStateProvider.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 5cf74aca90..e93b74d934 100644 --- 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 @@ -23,7 +23,8 @@ open class PermissionsViewStateProvider : PreviewParameterProvider get() = sequenceOf( aPermissionsState(), - // Add other state here + aPermissionsState().copy(shouldShowRationale = true), + aPermissionsState().copy(permissionAlreadyDenied = true), ) } From 7aca1d6bd54c9b9b4dec9ce6fdb85891e5ac8cb9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:46:44 +0200 Subject: [PATCH 070/119] Let this module generate screenshot preview. --- libraries/permissions/api/build.gradle.kts | 3 +++ ...oup_PermissionsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...oup_PermissionsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...oup_PermissionsViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...up_PermissionsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...up_PermissionsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...up_PermissionsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ 7 files changed, 21 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_0,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.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_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.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_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.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_0,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.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_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.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png diff --git a/libraries/permissions/api/build.gradle.kts b/libraries/permissions/api/build.gradle.kts index d86f790a44..99bc60a2eb 100644 --- a/libraries/permissions/api/build.gradle.kts +++ b/libraries/permissions/api/build.gradle.kts @@ -16,6 +16,7 @@ plugins { id("io.element.android-compose-library") + alias(libs.plugins.ksp) } android { @@ -27,4 +28,6 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) + + ksp(libs.showkase.processor) } diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_0,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.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4abfbacbbc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d91a9a9decf08f9bd9301d5282e889fb4e12d4270e8dc7c4b8b24de0b6059126 +size 24662 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_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.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cc898176e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44aa08cd01010ca90fb9ca33cb724dd0ebc6d523eff25b40e65f73f3ca280c19 +size 34209 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_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.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3141cfc7aa --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cca345dc82e59d0a87411f00e39e12128b4e61d2d7343c8d7af3d96e54038ca0 +size 28591 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_0,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.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..22ad0f0059 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6548a7cc39e0861de6af8e55bc00424d8835e5aa3d99d4e7c68db682e054d677 +size 24542 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_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.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4a8614354a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:817a15eb656b7dbcc23a0a62528804d346ca045d9c765579829ac5d3e8d16974 +size 34224 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_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.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dbd6530f91 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:775380d09f801b563ba5444e046088963d6830c9bf2fa98cf106fae28f94784a +size 28600 From 0cf2bfea0e8ffb82f4a25373fc2818e0603407d0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:48:12 +0200 Subject: [PATCH 071/119] Fix bad log. --- .../libraries/permissions/impl/DefaultPermissionsPresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9e91869549..e3fc4ed12b 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 @@ -79,7 +79,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor( if (!result) { // Should show rational true -> denied. if (permissionState?.status?.shouldShowRationale == true) { - Timber.tag("PERMISSION").w("onPermissionResult: reset the store") + Timber.tag("PERMISSION").w("onPermissionResult: setPermissionDenied to true") localCoroutineScope.launch { permissionsStore.setPermissionDenied(permission, true) } From bf63db458ccfc641691488590cc7d52f516cba40 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:50:38 +0200 Subject: [PATCH 072/119] Log: create and use a `loggerTag` --- .../permissions/impl/DefaultPermissionsPresenter.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 e3fc4ed12b..50012f01b7 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 @@ -33,6 +33,7 @@ import com.squareup.anvil.annotations.ContributesBinding import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.AppScope import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsPresenter @@ -40,6 +41,8 @@ import io.element.android.libraries.permissions.api.PermissionsState import kotlinx.coroutines.launch import timber.log.Timber +private val loggerTag = LoggerTag("DefaultPermissionsPresenter") + class DefaultPermissionsPresenter @AssistedInject constructor( @Assisted val permission: String, private val permissionsStore: PermissionsStore, @@ -71,7 +74,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor( var permissionState: PermissionState? = null fun onPermissionResult(result: Boolean) { - Timber.tag("PERMISSION").w("onPermissionResult: $result") + Timber.tag(loggerTag.value).d("onPermissionResult: $result") localCoroutineScope.launch { permissionsStore.setPermissionAsked(permission, true) } @@ -79,7 +82,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor( if (!result) { // Should show rational true -> denied. if (permissionState?.status?.shouldShowRationale == true) { - Timber.tag("PERMISSION").w("onPermissionResult: setPermissionDenied to true") + Timber.tag(loggerTag.value).d("onPermissionResult: setPermissionDenied to true") localCoroutineScope.launch { permissionsStore.setPermissionDenied(permission, true) } @@ -102,7 +105,6 @@ class DefaultPermissionsPresenter @AssistedInject constructor( val showDialog = rememberSaveable { mutableStateOf(permissionState.status !is PermissionStatus.Granted) } fun handleEvents(event: PermissionsEvents) { - Timber.tag("PERMISSION").w("New event: $event") when (event) { PermissionsEvents.CloseDialog -> { showDialog.value = false @@ -123,7 +125,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor( permissionAlreadyDenied = isAlreadyDenied, eventSink = ::handleEvents ).also { - Timber.tag("PERMISSION").w("New state: $it") + Timber.tag(loggerTag.value).d("New state: $it") } } From bec41f1c6a166c6f1073552754e18e59019be95a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:54:37 +0200 Subject: [PATCH 073/119] Move dependency declaration to the gradle catalog. --- gradle/libs.versions.toml | 2 ++ libraries/push/impl/build.gradle.kts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f81ce0e835..16231eedc5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -133,6 +133,8 @@ sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", vers sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.3" sqlite = "androidx.sqlite:sqlite:2.3.1" +unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1" +gujun_span = "me.gujun.android:span:1.7" # Di inject = "javax.inject:javax.inject:1" diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 0968ac4344..159500a515 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -49,7 +49,7 @@ dependencies { implementation(projects.services.appnavstate.api) implementation(projects.services.toolbox.api) - api("me.gujun.android:span:1.7") { + api(libs.gujun.span) { exclude(group = "com.android.support", module = "support-annotations") } @@ -57,7 +57,7 @@ dependencies { implementation("com.google.firebase:firebase-messaging-ktx") // UnifiedPush - api("com.github.UnifiedPush:android-connector:2.1.1") + api(libs.unifiedpush) testImplementation(libs.test.junit) testImplementation(libs.test.mockk) From 5bb504861ce0b78bf8bf2eec360f5ee97970b38c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:55:44 +0200 Subject: [PATCH 074/119] Use handy `toIntOrNull` --- .../android/libraries/push/impl/firebase/FirebasePushParser.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index 906816eb56..1f18b20652 100644 --- 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 @@ -16,7 +16,6 @@ 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 @@ -25,7 +24,7 @@ class FirebasePushParser @Inject constructor() { val pushDataFirebase = PushDataFirebase( eventId = message["event_id"], roomId = message["room_id"], - unread = message["unread"]?.let { tryOrNull { Integer.parseInt(it) } }, + unread = message["unread"]?.toIntOrNull(), clientSecret = message["cs"], ) return pushDataFirebase.toPushData() From 1134f50090ff812f9bac7bc2a8ead7912a79211e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 16:09:32 +0200 Subject: [PATCH 075/119] PushData must have valid Event and Room ids. --- .../push/impl/firebase/FirebasePushParser.kt | 2 +- .../push/impl/firebase/PushDataFirebase.kt | 17 +++++++------ .../VectorFirebaseMessagingService.kt | 7 ++++-- .../libraries/push/impl/push/PushData.kt | 10 ++++---- .../impl/unifiedpush/PushDataUnifiedPush.kt | 17 +++++++------ .../VectorUnifiedPushMessagingReceiver.kt | 9 ++++--- .../impl/firebase/FirebasePushParserTest.kt | 25 ++++++------------- .../impl/unifiedpush/UnifiedPushParserTest.kt | 21 ++++------------ 8 files changed, 48 insertions(+), 60 deletions(-) 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 index 1f18b20652..8659c299ae 100644 --- 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 @@ -20,7 +20,7 @@ import io.element.android.libraries.push.impl.push.PushData import javax.inject.Inject class FirebasePushParser @Inject constructor() { - fun parse(message: Map): PushData { + fun parse(message: Map): PushData? { val pushDataFirebase = PushDataFirebase( eventId = message["event_id"], roomId = message["room_id"], diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt index bcf48bab15..739c161e79 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.push.impl.firebase -import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.core.asRoomId import io.element.android.libraries.push.impl.push.PushData @@ -41,9 +40,13 @@ data class PushDataFirebase( val clientSecret: String? ) -fun PushDataFirebase.toPushData() = PushData( - eventId = eventId?.takeIf { MatrixPatterns.isEventId(it) }?.asEventId(), - roomId = roomId?.takeIf { MatrixPatterns.isRoomId(it) }?.asRoomId(), - unread = unread, - clientSecret = clientSecret, -) +fun PushDataFirebase.toPushData(): PushData? { + val safeEventId = eventId?.asEventId() ?: return null + val safeRoomId = roomId?.asRoomId() ?: return null + return PushData( + eventId = safeEventId, + roomId = safeRoomId, + unread = unread, + clientSecret = clientSecret, + ) +} 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 8769baa947..2ccf6f2505 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 @@ -53,8 +53,11 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { Timber.tag(loggerTag.value).d("New Firebase message") coroutineScope.launch { - pushParser.parse(message.data).let { - pushHandler.handle(it) + val pushData = pushParser.parse(message.data) + if (pushData == null) { + Timber.tag(loggerTag.value).w("Invalid data received from Firebase") + } else { + pushHandler.handle(pushData) } } } 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 864155e522..aaf6d65c08 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 @@ -22,14 +22,14 @@ import io.element.android.libraries.matrix.api.core.RoomId /** * 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 eventId The Event Id. + * @property roomId The Room Id. * @property unread Number of unread message. - * @property clientSecret A client secret, used to determine which user should receive the notification. + * @property clientSecret data used when the pusher was configured, to be able to determine the session. */ data class PushData( - val eventId: EventId?, - val roomId: RoomId?, + val eventId: EventId, + val roomId: RoomId, val unread: Int?, val clientSecret: String?, ) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt index 0bc6a6bce5..73d5f0286a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.push.impl.unifiedpush -import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.core.asRoomId import io.element.android.libraries.push.impl.push.PushData @@ -56,9 +55,13 @@ data class PushDataUnifiedPushCounts( @SerialName("unread") val unread: Int? = null ) -fun PushDataUnifiedPush.toPushData() = PushData( - eventId = notification?.eventId?.takeIf { MatrixPatterns.isEventId(it) }?.asEventId(), - roomId = notification?.roomId?.takeIf { MatrixPatterns.isRoomId(it) }?.asRoomId(), - unread = notification?.counts?.unread, - clientSecret = null // TODO EAx check how client secret will be sent through UnifiedPush -) +fun PushDataUnifiedPush.toPushData(): PushData? { + val safeEventId = notification?.eventId?.asEventId() ?: return null + val safeRoomId = notification.roomId?.asRoomId() ?: return null + return PushData( + eventId = safeEventId, + roomId = safeRoomId, + 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/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index 81dd389e78..acb438cc70 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 @@ -67,10 +67,11 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { override fun onMessage(context: Context, message: ByteArray, instance: String) { Timber.tag(loggerTag.value).d("New message") coroutineScope.launch { - pushParser.parse(message)?.let { - pushHandler.handle(it) - } ?: run { - Timber.tag(loggerTag.value).w("Invalid received data Json format") + val pushData = pushParser.parse(message) + if (pushData == null) { + Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush") + } else { + pushHandler.handle(pushData) } } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt index 9d02913cf8..b14e067dae 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt @@ -30,18 +30,11 @@ class FirebasePushParserTest { clientSecret = "a-secret" ) - private val emptyData = PushData( - eventId = null, - roomId = null, - unread = null, - clientSecret = null - ) - @Test fun `test edge cases Firebase`() { val pushParser = FirebasePushParser() // Empty Json - assertThat(pushParser.parse(emptyMap())).isEqualTo(emptyData) + assertThat(pushParser.parse(emptyMap())).isNull() // Bad Json assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("unread", "str"))).isEqualTo(validData.copy(unread = null)) // Extra data @@ -57,31 +50,27 @@ class FirebasePushParserTest { @Test fun `test empty roomId`() { val pushParser = FirebasePushParser() - val expected = validData.copy(roomId = null) - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", null))).isEqualTo(expected) - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", ""))).isEqualTo(expected) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", null))).isNull() + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", ""))).isNull() } @Test fun `test invalid roomId`() { val pushParser = FirebasePushParser() - val expected = validData.copy(roomId = null) - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain"))).isEqualTo(expected) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain"))).isNull() } @Test fun `test empty eventId`() { val pushParser = FirebasePushParser() - val expected = validData.copy(eventId = null) - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", null))).isEqualTo(expected) - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", ""))).isEqualTo(expected) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", null))).isNull() + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", ""))).isNull() } @Test fun `test invalid eventId`() { val pushParser = FirebasePushParser() - val expected = validData.copy(eventId = null) - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId"))).isEqualTo(expected) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId"))).isNull() } companion object { diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt index 00c92bb08a..f9275de4b2 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt @@ -31,20 +31,13 @@ class UnifiedPushParserTest { clientSecret = null ) - private val emptyData = PushData( - eventId = null, - roomId = null, - unread = null, - clientSecret = null - ) - @Test fun `test edge cases UnifiedPush`() { val pushParser = UnifiedPushParser() // Empty string assertThat(pushParser.parse("".toByteArray())).isNull() // Empty Json - assertThat(pushParser.parse("{}".toByteArray())).isEqualTo(emptyData) + assertThat(pushParser.parse("{}".toByteArray())).isNull() // Bad Json assertThat(pushParser.parse("ABC".toByteArray())).isNull() } @@ -58,29 +51,25 @@ class UnifiedPushParserTest { @Test fun `test empty roomId`() { val pushParser = UnifiedPushParser() - val expected = validData.copy(roomId = null) - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray())).isEqualTo(expected) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray())).isNull() } @Test fun `test invalid roomId`() { val pushParser = UnifiedPushParser() - val expected = validData.copy(roomId = null) - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"))).isEqualTo(expected) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"))).isNull() } @Test fun `test empty eventId`() { val pushParser = UnifiedPushParser() - val expected = validData.copy(eventId = null) - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""))).isEqualTo(expected) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""))).isNull() } @Test fun `test invalid eventId`() { val pushParser = UnifiedPushParser() - val expected = validData.copy(eventId = null) - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"))).isEqualTo(expected) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"))).isNull() } companion object { From 219b97eea7f7d78c4093302b1315a177a3ba0fd7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 20:42:59 +0200 Subject: [PATCH 076/119] Split push module into several modules: Firebase, UnifiedPush, store --- appnav/build.gradle.kts | 1 + .../appnav/loggedin/LoggedInPresenter.kt | 4 +- .../appnav/loggedin/LoggedInPresenterTest.kt | 7 ++- libraries/push/api/build.gradle.kts | 1 + .../android/libraries/push/api/PushService.kt | 13 ++++- libraries/push/impl/build.gradle.kts | 6 +-- .../push/impl/src/main/AndroidManifest.xml | 50 +----------------- .../libraries/push/impl/DefaultPushService.kt | 20 ++++---- .../android/libraries/push/impl/FcmHelper.kt | 49 ------------------ .../libraries/push/impl/PushersManager.kt | 36 ++++--------- .../libraries/push/impl/config/PushConfig.kt | 17 ------- .../{PushHandler.kt => DefaultPushHandler.kt} | 11 ++-- libraries/pushproviders/api/build.gradle.kts | 28 ++++++++++ .../libraries/push/providers/api}/PushData.kt | 10 ++-- .../push/providers/api/PushHandler.kt | 21 ++++++++ .../push/providers/api/PushProvider.kt | 27 ++++++++++ .../push/providers/api/PusherSubscriber.kt | 23 +++++++++ .../pushproviders/firebase/build.gradle.kts | 48 +++++++++++++++++ .../firebase/src/main/AndroidManifest.xml | 31 +++++++++++ .../EnsureFcmTokenIsRetrievedUseCase.kt | 20 ++++---- .../push/providers/firebase/FirebaseConfig.kt | 27 ++++++++++ .../providers}/firebase/FirebasePushParser.kt | 6 +-- .../firebase/FirebasePushProvider.kt | 44 ++++++++++++++++ .../providers/firebase/FirebaseSetPusher.kt | 51 +++++++++++++++++++ .../providers/firebase}/GoogleFcmHelper.kt | 20 +++----- .../providers}/firebase/PushDataFirebase.kt | 4 +- .../VectorFirebaseMessagingService.kt | 14 ++--- .../VectorFirebaseMessagingServiceBindings.kt | 2 +- .../providers/firebase/di/FirebaseModule.kt | 33 ++++++++++++ .../firebase/FirebasePushParserTest.kt | 4 +- .../unifiedpush/build.gradle.kts | 49 ++++++++++++++++++ .../unifiedpush/src/main/AndroidManifest.xml | 47 +++++++++++++++++ .../unifiedpush/GuardServiceStarter.kt | 2 +- .../unifiedpush/KeepInternalDistributor.kt | 4 +- .../unifiedpush/PushDataUnifiedPush.kt | 4 +- .../unifiedpush/RegisterUnifiedPushUseCase.kt | 8 +-- .../unifiedpush/UnifiedPushConfig.kt | 27 ++++++++++ .../unifiedpush}/UnifiedPushHelper.kt | 26 ++++++---- .../unifiedpush/UnifiedPushParser.kt | 6 +-- .../unifiedpush/UnifiedPushProvider.kt | 31 +++++++++++ .../unifiedpush}/UnifiedPushStore.kt | 4 +- .../UnregisterUnifiedPushUseCase.kt | 15 ++---- .../VectorUnifiedPushMessagingReceiver.kt | 32 ++++++------ ...torUnifiedPushMessagingReceiverBindings.kt | 2 +- .../unifiedpush/di/UnifiedPushModule.kt | 33 ++++++++++++ .../unifiedpush/UnifiedPushParserTest.kt | 4 +- libraries/pushstore/api/build.gradle.kts | 26 ++++++++++ .../libraries/pushstore/api}/UserPushStore.kt | 9 +--- .../pushstore/api/UserPushStoreFactory.kt | 24 +++++++++ libraries/pushstore/impl/build.gradle.kts | 38 ++++++++++++++ .../impl/DefaultUserPushStoreFactory.kt} | 12 +++-- .../pushstore/impl}/UserPushStoreDataStore.kt | 7 +-- .../kotlin/extension/DependencyHandleScope.kt | 6 +++ 53 files changed, 768 insertions(+), 276 deletions(-) delete mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/FcmHelper.kt rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/{PushHandler.kt => DefaultPushHandler.kt} (93%) create mode 100644 libraries/pushproviders/api/build.gradle.kts rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push => pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api}/PushData.kt (85%) create mode 100644 libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushHandler.kt create mode 100644 libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt create mode 100644 libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt create mode 100644 libraries/pushproviders/firebase/build.gradle.kts create mode 100644 libraries/pushproviders/firebase/src/main/AndroidManifest.xml rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers}/firebase/EnsureFcmTokenIsRetrievedUseCase.kt (66%) create mode 100644 libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers}/firebase/FirebasePushParser.kt (85%) create mode 100644 libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt create mode 100644 libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseSetPusher.kt rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase}/GoogleFcmHelper.kt (87%) mode change 100755 => 100644 rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers}/firebase/PushDataFirebase.kt (91%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers}/firebase/VectorFirebaseMessagingService.kt (82%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers}/firebase/VectorFirebaseMessagingServiceBindings.kt (93%) create mode 100644 libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/di/FirebaseModule.kt rename libraries/{push/impl/src/test/kotlin/io/element/android/libraries/push/impl => pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers}/firebase/FirebasePushParserTest.kt (95%) create mode 100644 libraries/pushproviders/unifiedpush/build.gradle.kts create mode 100644 libraries/pushproviders/unifiedpush/src/main/AndroidManifest.xml rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/GuardServiceStarter.kt (93%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/KeepInternalDistributor.kt (90%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/PushDataUnifiedPush.kt (93%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/RegisterUnifiedPushUseCase.kt (86%) create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush}/UnifiedPushHelper.kt (90%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/UnifiedPushParser.kt (85%) create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush}/UnifiedPushStore.kt (95%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/UnregisterUnifiedPushUseCase.kt (72%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/VectorUnifiedPushMessagingReceiver.kt (82%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt (93%) create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/di/UnifiedPushModule.kt rename libraries/{push/impl/src/test/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers}/unifiedpush/UnifiedPushParserTest.kt (95%) create mode 100644 libraries/pushstore/api/build.gradle.kts rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore => pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api}/UserPushStore.kt (72%) create mode 100644 libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt create mode 100644 libraries/pushstore/impl/build.gradle.kts rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt => pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt} (78%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore => pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl}/UserPushStoreDataStore.kt (91%) diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 8ece3e5841..17efdc15fc 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.push.api) + implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.matrixui) implementation(projects.libraries.uiStrings) 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 b628f19bd1..552420abf8 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,9 @@ class LoggedInPresenter @Inject constructor( override fun present(): LoggedInState { LaunchedEffect(Unit) { // Ensure pusher is registered - pushService.registerFirebasePusher(matrixClient) + // TODO Register with Firebase for now + val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect + pushService.registerWith(matrixClient, pushProvider, pushProvider.getDistributorNames().first()) } val permissionsState = postNotificationPermissionsPresenter.present() 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 index 64767eaafc..595f4850b2 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -27,6 +27,7 @@ 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 io.element.android.libraries.push.providers.api.PushProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test @@ -55,7 +56,11 @@ class LoggedInPresenterTest { override fun notificationStyleChanged() { } - override suspend fun registerFirebasePusher(matrixClient: MatrixClient) { + override fun getAvailablePushProviders(): List { + return emptyList() + } + + override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) { } override suspend fun testPush() { diff --git a/libraries/push/api/build.gradle.kts b/libraries/push/api/build.gradle.kts index be1bbc13ef..0c5df8fb25 100644 --- a/libraries/push/api/build.gradle.kts +++ b/libraries/push/api/build.gradle.kts @@ -26,4 +26,5 @@ dependencies { implementation(libs.androidx.corektx) implementation(libs.coroutines.core) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.pushproviders.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 cf5792be35..7eeca6e0a3 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 @@ -17,12 +17,21 @@ package io.element.android.libraries.push.api import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.providers.api.PushProvider interface PushService { + // TODO Move away fun notificationStyleChanged() - // Ensure pusher is registered - suspend fun registerFirebasePusher(matrixClient: MatrixClient) + fun getAvailablePushProviders(): List + /** + * Will unregister any previous pusher and register a new one with the provided [PushProvider]. + * + * The method has effect only if the [PushProvider] is different than the current one. + */ + suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) + + // TODO Move away suspend fun testPush() } diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 159500a515..44fc21fb2b 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -43,6 +43,8 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.network) implementation(projects.libraries.matrix.api) + api(projects.libraries.pushproviders.api) + api(projects.libraries.pushstore.api) api(projects.libraries.push.api) implementation(projects.services.analytics.api) @@ -53,12 +55,10 @@ dependencies { exclude(group = "com.android.support", module = "support-annotations") } + // TODO Remove implementation(platform(libs.google.firebase.bom)) implementation("com.google.firebase:firebase-messaging-ktx") - // UnifiedPush - api(libs.unifiedpush) - testImplementation(libs.test.junit) testImplementation(libs.test.mockk) testImplementation(libs.test.truth) diff --git a/libraries/push/impl/src/main/AndroidManifest.xml b/libraries/push/impl/src/main/AndroidManifest.xml index a386a89caf..6085ffe4a4 100644 --- a/libraries/push/impl/src/main/AndroidManifest.xml +++ b/libraries/push/impl/src/main/AndroidManifest.xml @@ -14,63 +14,15 @@ ~ limitations under the License. --> - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 7924ddf996..9bf4afa7ea 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,27 +20,29 @@ 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 io.element.android.libraries.push.providers.api.PushProvider import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultPushService @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val pushersManager: PushersManager, - private val fcmHelper: FcmHelper, + private val pushProviders: Set<@JvmSuppressWildcards PushProvider>, ) : PushService { override fun notificationStyleChanged() { notificationDrawerManager.notificationStyleChanged() } - 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 fun getAvailablePushProviders(): List { + // TODO Sort by priority? + return pushProviders.toList() + } + + override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) { + // TODO Get current push provider, compare with provided one, then unregister and register if different, and store change + + pushProvider.registerWith(matrixClient, distributorName) } override suspend fun testPush() { 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 deleted file mode 100644 index 9b8b6c2281..0000000000 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/FcmHelper.kt +++ /dev/null @@ -1,49 +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 - -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/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index f1ac346909..532a904c82 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,7 +16,9 @@ package io.element.android.libraries.push.impl +import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.di.AppScope 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.EventId @@ -26,10 +28,9 @@ 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.log.pushLoggerTag 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.push.providers.api.PusherSubscriber +import io.element.android.libraries.pushstore.api.UserPushStoreFactory 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 @@ -38,8 +39,9 @@ internal const val DEFAULT_PUSHER_FILE_TAG = "mobile" private val loggerTag = LoggerTag("PushersManager", pushLoggerTag) +@ContributesBinding(AppScope::class) class PushersManager @Inject constructor( - private val unifiedPushHelper: UnifiedPushHelper, + // private val unifiedPushHelper: UnifiedPushHelper, // private val localeProvider: LocaleProvider, private val appNameProvider: AppNameProvider, // private val getDeviceInfoUseCase: GetDeviceInfoUseCase, @@ -48,14 +50,14 @@ class PushersManager @Inject constructor( private val sessionStore: SessionStore, private val matrixAuthenticationService: MatrixAuthenticationService, private val userPushStoreFactory: UserPushStoreFactory, - private val fcmHelper: FcmHelper, -) { +): PusherSubscriber { + // TODO Move this to the PushProvider API suspend fun testPush() { pushGatewayNotifyRequest.execute( PushGatewayNotifyRequest.Params( - url = unifiedPushHelper.getPushGateway() ?: return, + url = "TODO", // unifiedPushHelper.getPushGateway() ?: return, appId = PushConfig.pusher_app_id, - pushKey = unifiedPushHelper.getEndpointOrToken().orEmpty(), + pushKey = "TODO", // unifiedPushHelper.getEndpointOrToken().orEmpty(), eventId = TEST_EVENT_ID ) ) @@ -73,26 +75,10 @@ class PushersManager @Inject constructor( TODO() } - suspend fun onNewFirebaseToken(firebaseToken: String) { - fcmHelper.storeFcmToken(firebaseToken) - - // Register the pusher for all the sessions - sessionStore.getAllSessions().toUserList().forEach { userId -> - val userDataStore = userPushStoreFactory.create(userId) - if (userDataStore.isFirebase()) { - matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client -> - registerPusher(client, firebaseToken, PushConfig.pusher_http_url) - } - } else { - Timber.tag(loggerTag.value).d("This session is not using Firebase pusher") - } - } - } - /** * Register a pusher to the server if not done yet. */ - suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) { + override suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) { val userDataStore = userPushStoreFactory.create(matrixClient.sessionId.value) if (userDataStore.getCurrentRegisteredPushKey() == pushKey) { Timber.tag(loggerTag.value).d("Unnecessary to register again the same 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 index d2d1c96506..823dd7f693 100644 --- 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 @@ -17,25 +17,8 @@ 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/push/PushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt similarity index 93% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index b4c9716b62..30ee74f3de 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/DefaultPushHandler.kt @@ -21,9 +21,11 @@ import android.content.Intent import android.os.Handler import android.os.Looper import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.squareup.anvil.annotations.ContributesBinding 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.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.push.api.store.PushDataStore @@ -34,6 +36,8 @@ 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 io.element.android.libraries.push.providers.api.PushData +import io.element.android.libraries.push.providers.api.PushHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -43,7 +47,8 @@ import javax.inject.Inject private val loggerTag = LoggerTag("PushHandler", pushLoggerTag) -class PushHandler @Inject constructor( +@ContributesBinding(AppScope::class) +class DefaultPushHandler @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val notifiableEventResolver: NotifiableEventResolver, private val pushDataStore: PushDataStore, @@ -53,7 +58,7 @@ class PushHandler @Inject constructor( @ApplicationContext private val context: Context, private val buildMeta: BuildMeta, private val matrixAuthenticationService: MatrixAuthenticationService, -) { +): PushHandler { private val coroutineScope = CoroutineScope(SupervisorJob()) private val wifiDetector: WifiDetector = WifiDetector(context) @@ -68,7 +73,7 @@ class PushHandler @Inject constructor( * * @param pushData the data received in the push. */ - suspend fun handle(pushData: PushData) { + override suspend fun handle(pushData: PushData) { Timber.tag(loggerTag.value).d("## handling pushData") if (buildMeta.lowPrivacyLoggingEnabled) { diff --git a/libraries/pushproviders/api/build.gradle.kts b/libraries/pushproviders/api/build.gradle.kts new file mode 100644 index 0000000000..08d397b383 --- /dev/null +++ b/libraries/pushproviders/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.providers.api" +} + +dependencies { + implementation(projects.libraries.core) + implementation(projects.libraries.matrix.api) +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushData.kt similarity index 85% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt rename to libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushData.kt index aaf6d65c08..b304d10b34 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushData.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.push +package io.element.android.libraries.push.providers.api import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -28,8 +28,8 @@ import io.element.android.libraries.matrix.api.core.RoomId * @property clientSecret data used when the pusher was configured, to be able to determine the session. */ data class PushData( - val eventId: EventId, - val roomId: RoomId, - val unread: Int?, - val clientSecret: String?, + val eventId: EventId, + val roomId: RoomId, + val unread: Int?, + val clientSecret: String?, ) diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushHandler.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushHandler.kt new file mode 100644 index 0000000000..09ca420a1f --- /dev/null +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushHandler.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.providers.api + +interface PushHandler { + suspend fun handle(pushData: PushData) +} diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt new file mode 100644 index 0000000000..2b90c3d5b3 --- /dev/null +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.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.push.providers.api + +import io.element.android.libraries.matrix.api.MatrixClient + +/** + * This is the main API for this module + */ +interface PushProvider { + fun getDistributorNames(): List + suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) +} diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt new file mode 100644 index 0000000000..805244e0ed --- /dev/null +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.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.providers.api + +import io.element.android.libraries.matrix.api.MatrixClient + +interface PusherSubscriber { + suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) +} diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts new file mode 100644 index 0000000000..65360d5465 --- /dev/null +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -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. + */ + +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) + // kotlin("plugin.serialization") version "1.8.10" +} + +android { + namespace = "io.element.android.libraries.push.providers.firebase" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.dagger) + implementation(libs.androidx.corektx) + implementation(projects.libraries.architecture) + implementation(projects.libraries.core) + implementation(projects.libraries.di) + implementation(projects.libraries.matrix.api) + + implementation(projects.libraries.pushstore.api) + implementation(projects.libraries.pushproviders.api) + + implementation(platform(libs.google.firebase.bom)) + implementation("com.google.firebase:firebase-messaging-ktx") + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(projects.libraries.matrix.test) +} diff --git a/libraries/pushproviders/firebase/src/main/AndroidManifest.xml b/libraries/pushproviders/firebase/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..40dc254644 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/EnsureFcmTokenIsRetrievedUseCase.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/EnsureFcmTokenIsRetrievedUseCase.kt similarity index 66% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/EnsureFcmTokenIsRetrievedUseCase.kt rename to libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/EnsureFcmTokenIsRetrievedUseCase.kt index 9e9b28ecb8..e859976789 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/EnsureFcmTokenIsRetrievedUseCase.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/EnsureFcmTokenIsRetrievedUseCase.kt @@ -14,24 +14,22 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.firebase +package io.element.android.libraries.push.providers.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 +// TODO class EnsureFcmTokenIsRetrievedUseCase @Inject constructor( - private val unifiedPushHelper: UnifiedPushHelper, - private val fcmHelper: FcmHelper, +// 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)) - } - } +// fun execute(pushersManager: PushersManager, registerPusher: Boolean) { +// if (unifiedPushHelper.isEmbeddedDistributor()) { +// fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher)) +// } +// } private fun shouldAddHttpPusher(registerPusher: Boolean) = if (registerPusher) { /* diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt new file mode 100644 index 0000000000..27463825d6 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.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.push.providers.firebase + +object FirebaseConfig { + /** + * It is the push gateway for firebase. + * Note: pusher_http_url should have path '/_matrix/push/v1/notify' --> + */ + const val pusher_http_url: String = "https://matrix.org/_matrix/push/v1/notify" + + const val internalName = "NOTIFICATION_METHOD_FIREBASE" +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParser.kt similarity index 85% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt rename to libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParser.kt index 8659c299ae..d3af7d8448 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParser.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,9 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.firebase +package io.element.android.libraries.push.providers.firebase -import io.element.android.libraries.push.impl.push.PushData +import io.element.android.libraries.push.providers.api.PushData import javax.inject.Inject class FirebasePushParser @Inject constructor() { diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt new file mode 100644 index 0000000000..855762e5ab --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.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.libraries.push.providers.firebase + +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.providers.api.PushProvider +import io.element.android.libraries.push.providers.api.PusherSubscriber +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("FirebasePushProvider") + +class FirebasePushProvider @Inject constructor( + private val googleFcmHelper: GoogleFcmHelper, + private val pusherSubscriber: PusherSubscriber, +) : PushProvider { + + override fun getDistributorNames(): List { + // Must return an non-empty list for now + return listOf("unused") + } + + override suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) { + val pushKey = googleFcmHelper.getFcmToken() ?: return Unit.also { + Timber.tag(loggerTag.value).w("Unable to register pusher, Firebase token is not known.") + } + pusherSubscriber.registerPusher(matrixClient, pushKey, FirebaseConfig.pusher_http_url) + } +} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseSetPusher.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseSetPusher.kt new file mode 100644 index 0000000000..ea6937551a --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseSetPusher.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.push.providers.firebase + +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.push.providers.api.PusherSubscriber +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.api.toUserList +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("FirebaseSetPusher") + +// TODO Rename +class FirebaseSetPusher @Inject constructor( + private val pusherSubscriber: PusherSubscriber, + private val sessionStore: SessionStore, + private val userPushStoreFactory: UserPushStoreFactory, + private val matrixAuthenticationService: MatrixAuthenticationService, +) { + suspend fun onNewFirebaseToken(firebaseToken: String) { + // Register the pusher for all the sessions + sessionStore.getAllSessions().toUserList().forEach { userId -> + val userDataStore = userPushStoreFactory.create(userId) + if (userDataStore.getNotificationMethod() == FirebaseConfig.internalName) { + matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client -> + pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.pusher_http_url) + } + } else { + Timber.tag(loggerTag.value).d("This session is not using Firebase pusher") + } + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/GoogleFcmHelper.kt old mode 100755 new mode 100644 similarity index 87% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt rename to libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/GoogleFcmHelper.kt index 6c73607196..0772ac0603 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/GoogleFcmHelper.kt @@ -14,44 +14,37 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl +package io.element.android.libraries.push.providers.firebase 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 kotlinx.coroutines.runBlocking -import timber.log.Timber import javax.inject.Inject /** * 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) +// TODO Rename to store? class GoogleFcmHelper @Inject constructor( @ApplicationContext private val context: Context, @DefaultPreferences private val sharedPrefs: SharedPreferences, -) : FcmHelper { - override fun isFirebaseAvailable(): Boolean = true - - override fun getFcmToken(): String? { +) { + fun getFcmToken(): String? { return sharedPrefs.getString(PREFS_KEY_FCM_TOKEN, null) } - override fun storeFcmToken(token: String?) { + 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)) { @@ -76,6 +69,7 @@ class GoogleFcmHelper @Inject constructor( 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 diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/PushDataFirebase.kt similarity index 91% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt rename to libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/PushDataFirebase.kt index 739c161e79..5c336e7dc6 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/PushDataFirebase.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.firebase +package io.element.android.libraries.push.providers.firebase import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.core.asRoomId -import io.element.android.libraries.push.impl.push.PushData +import io.element.android.libraries.push.providers.api.PushData /** * In this case, the format is: diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt similarity index 82% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt rename to libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt index 2ccf6f2505..02bb84c541 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt @@ -14,27 +14,26 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.firebase +package io.element.android.libraries.push.providers.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.impl.PushersManager -import io.element.android.libraries.push.impl.log.pushLoggerTag -import io.element.android.libraries.push.impl.push.PushHandler +import io.element.android.libraries.push.providers.api.PushHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("Firebase", pushLoggerTag) +private val loggerTag = LoggerTag("Firebase") class VectorFirebaseMessagingService : FirebaseMessagingService() { - @Inject lateinit var pushersManager: PushersManager + @Inject lateinit var firebaseSetPusher: FirebaseSetPusher @Inject lateinit var pushParser: FirebasePushParser @Inject lateinit var pushHandler: PushHandler + @Inject lateinit var googleFcmHelper: GoogleFcmHelper private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -45,8 +44,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { override fun onNewToken(token: String) { Timber.tag(loggerTag.value).d("New Firebase token") + googleFcmHelper.storeFcmToken(token) coroutineScope.launch { - pushersManager.onNewFirebaseToken(token) + firebaseSetPusher.onNewFirebaseToken(token) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingServiceBindings.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingServiceBindings.kt similarity index 93% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingServiceBindings.kt rename to libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingServiceBindings.kt index aef87e7df3..e17cc922ee 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingServiceBindings.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingServiceBindings.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.firebase +package io.element.android.libraries.push.providers.firebase import com.squareup.anvil.annotations.ContributesTo import io.element.android.libraries.di.AppScope diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/di/FirebaseModule.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/di/FirebaseModule.kt new file mode 100644 index 0000000000..9e36754101 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/di/FirebaseModule.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.providers.firebase.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.providers.api.PushProvider +import io.element.android.libraries.push.providers.firebase.FirebasePushProvider + +@Module +@ContributesTo(AppScope::class) +interface FirebaseModule { + @Binds + @IntoSet + fun bind(pushProvider: FirebasePushProvider): PushProvider +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt similarity index 95% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt rename to libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt index b14e067dae..a6525657c8 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.firebase +package io.element.android.libraries.push.providers.firebase import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.push.impl.push.PushData +import io.element.android.libraries.push.providers.api.PushData import org.junit.Test class FirebasePushParserTest { diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts new file mode 100644 index 0000000000..6817d0aad6 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -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. + */ + +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) + kotlin("plugin.serialization") version "1.8.10" +} + +android { + namespace = "io.element.android.libraries.push.providers.unifiedpush" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.dagger) + implementation(projects.libraries.core) + implementation(projects.libraries.matrix.api) + + implementation(projects.libraries.pushproviders.api) + implementation(projects.libraries.architecture) + implementation(projects.libraries.core) + implementation(projects.services.toolbox.api) + + implementation(libs.serialization.json) + + // UnifiedPush library + api(libs.unifiedpush) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(projects.libraries.matrix.test) +} diff --git a/libraries/pushproviders/unifiedpush/src/main/AndroidManifest.xml b/libraries/pushproviders/unifiedpush/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..719733ab3e --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/GuardServiceStarter.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/GuardServiceStarter.kt similarity index 93% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/GuardServiceStarter.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/GuardServiceStarter.kt index 08bd4a8326..f92468d047 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/GuardServiceStarter.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/GuardServiceStarter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.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/unifiedpush/KeepInternalDistributor.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/KeepInternalDistributor.kt similarity index 90% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/KeepInternalDistributor.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/KeepInternalDistributor.kt index de66ed3914..d2e0713f74 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/KeepInternalDistributor.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/KeepInternalDistributor.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.unifiedpush +package io.element.android.libraries.push.providers.unifiedpush import android.content.BroadcastReceiver import android.content.Context diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt similarity index 93% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt index 73d5f0286a..6d5ecb1db3 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.unifiedpush import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.core.asRoomId -import io.element.android.libraries.push.impl.push.PushData +import io.element.android.libraries.push.providers.api.PushData import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/RegisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt similarity index 86% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/RegisterUnifiedPushUseCase.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt index 50ca94f30d..a80d9ba865 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/RegisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt @@ -14,11 +14,10 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.unifiedpush 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 @@ -37,11 +36,6 @@ class RegisterUnifiedPushUseCase @Inject constructor( return RegisterUnifiedPushResult.Success } - if (!PushConfig.allowExternalUnifiedPushDistributors) { - saveAndRegisterApp(context.packageName) - return RegisterUnifiedPushResult.Success - } - if (UnifiedPush.getDistributor(context).isNotEmpty()) { registerApp() return RegisterUnifiedPushResult.Success diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt new file mode 100644 index 0000000000..73c31f430c --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.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.push.providers.unifiedpush + +object UnifiedPushConfig { + /** + * 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" + + const val internalName = "NOTIFICATION_METHOD_UNIFIEDPUSH" +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt similarity index 90% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt index 12ed3f1993..dce17015b7 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.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,10 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl +package io.element.android.libraries.push.providers.unifiedpush 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.services.toolbox.api.strings.StringProvider import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -31,8 +29,6 @@ import javax.inject.Inject 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, ) { @@ -95,11 +91,14 @@ class UnifiedPushHelper @Inject constructor( // 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 @@ -132,19 +131,25 @@ class UnifiedPushHelper @Inject constructor( } fun getCurrentDistributorName(): String { + TODO() + /* return when { 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)) } + + */ } fun isEmbeddedDistributor(): Boolean { - return isInternalDistributor() && fcmHelper.isFirebaseAvailable() + TODO() + //return isInternalDistributor() && fcmHelper.isFirebaseAvailable() } fun isBackgroundSync(): Boolean { - return isInternalDistributor() && !fcmHelper.isFirebaseAvailable() + TODO() + //return isInternalDistributor() && !fcmHelper.isFirebaseAvailable() } private fun isInternalDistributor(): Boolean { @@ -168,12 +173,13 @@ class UnifiedPushHelper @Inject constructor( } fun getEndpointOrToken(): String? { - return if (isEmbeddedDistributor()) fcmHelper.getFcmToken() + // TODO + return if (isEmbeddedDistributor()) "" // fcmHelper.getFcmToken() else unifiedPushStore.getEndpoint() } fun getPushGateway(): String? { - return if (isEmbeddedDistributor()) PushConfig.pusher_http_url + 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/unifiedpush/UnifiedPushParser.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt similarity index 85% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParser.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt index 05cd8425b7..881862d473 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParser.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.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,10 +14,10 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.unifiedpush import io.element.android.libraries.core.data.tryOrNull -import io.element.android.libraries.push.impl.push.PushData +import io.element.android.libraries.push.providers.api.PushData import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import javax.inject.Inject diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt new file mode 100644 index 0000000000..a37e9da5e2 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.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.providers.unifiedpush + +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.providers.api.PushProvider +import javax.inject.Inject + +class UnifiedPushProvider @Inject constructor(): PushProvider { + override fun getDistributorNames(): List { + TODO("Not yet implemented") + } + + override suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) { + TODO("Not yet implemented") + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt similarity index 95% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt index 226d0c5669..31d7bbd63e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.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.providers.unifiedpush import android.content.Context import android.content.SharedPreferences diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnregisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt similarity index 72% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnregisterUnifiedPushUseCase.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt index 6cd1af1de3..b7cd592951 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnregisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt @@ -14,19 +14,9 @@ * limitations under the License. */ -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 +package io.element.android.libraries.push.providers.unifiedpush +/* class UnregisterUnifiedPushUseCase @Inject constructor( @ApplicationContext private val context: Context, private val pushDataStore: PushDataStore, @@ -50,3 +40,4 @@ class UnregisterUnifiedPushUseCase @Inject constructor( UnifiedPush.unregisterApp(context) } } + */ diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt similarity index 82% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index acb438cc70..d745df6e87 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt @@ -14,40 +14,29 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.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.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 io.element.android.libraries.push.providers.api.PushHandler 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("Unified", pushLoggerTag) +private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver") class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { - @Inject lateinit var pushersManager: PushersManager @Inject lateinit var pushParser: UnifiedPushParser - - //@Inject lateinit var activeSessionHolder: ActiveSessionHolder - @Inject lateinit var pushDataStore: PushDataStore + // @Inject lateinit var pushDataStore: PushDataStore @Inject lateinit var pushHandler: PushHandler @Inject lateinit var guardServiceStarter: GuardServiceStarter - @Inject lateinit var unifiedPushStore: UnifiedPushStore - @Inject lateinit var unifiedPushHelper: UnifiedPushHelper +// @Inject lateinit var unifiedPushStore: UnifiedPushStore +// @Inject lateinit var unifiedPushHelper: UnifiedPushHelper private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -77,6 +66,8 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { } override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { + TODO() + /* Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint") if (pushDataStore.areNotificationEnabledForDevice() /* TODO EAx && activeSessionHolder.hasActiveSession() */) { // If the endpoint has changed @@ -99,16 +90,22 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED pushDataStore.setFdroidSyncBackgroundMode(mode) guardServiceStarter.stop() + */ } override fun onRegistrationFailed(context: Context, instance: String) { + TODO() + /* 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) { + TODO() + /* Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered") val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME pushDataStore.setFdroidSyncBackgroundMode(mode) @@ -120,5 +117,6 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { 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/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt similarity index 93% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt index 90857d990d..603e297c6b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.unifiedpush import com.squareup.anvil.annotations.ContributesTo import io.element.android.libraries.di.AppScope diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/di/UnifiedPushModule.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/di/UnifiedPushModule.kt new file mode 100644 index 0000000000..9e34b349e3 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/di/UnifiedPushModule.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.providers.unifiedpush.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.providers.api.PushProvider +import io.element.android.libraries.push.providers.unifiedpush.UnifiedPushProvider + +@Module +@ContributesTo(AppScope::class) +interface UnifiedPushModule { + @Binds + @IntoSet + fun bind(pushProvider: UnifiedPushProvider): PushProvider +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt similarity index 95% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt rename to libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt index f9275de4b2..6b5c0db62f 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.unifiedpush import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.push.impl.push.PushData +import io.element.android.libraries.push.providers.api.PushData import org.junit.Test class UnifiedPushParserTest { diff --git a/libraries/pushstore/api/build.gradle.kts b/libraries/pushstore/api/build.gradle.kts new file mode 100644 index 0000000000..de7a852ee0 --- /dev/null +++ b/libraries/pushstore/api/build.gradle.kts @@ -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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.pushstore.api" +} + +dependencies { +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt similarity index 72% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt rename to libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt index 82c4beaf20..35ec23f80d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt @@ -14,10 +14,7 @@ * 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" +package io.element.android.libraries.pushstore.api /** * Store data related to push about a user. @@ -26,7 +23,7 @@ interface UserPushStore { /** * [NOTIFICATION_METHOD_FIREBASE] or [NOTIFICATION_METHOD_UNIFIEDPUSH]. */ - suspend fun getNotificationMethod(): String + suspend fun getNotificationMethod(): String? suspend fun setNotificationMethod(value: String) @@ -36,5 +33,3 @@ interface UserPushStore { suspend fun reset() } - -suspend fun UserPushStore.isFirebase(): Boolean = getNotificationMethod() == NOTIFICATION_METHOD_FIREBASE diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt new file mode 100644 index 0000000000..832180e850 --- /dev/null +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.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.pushstore.api + +/** + * Store data related to push about a user. + */ +interface UserPushStoreFactory { + fun create(userId: String): UserPushStore +} diff --git a/libraries/pushstore/impl/build.gradle.kts b/libraries/pushstore/impl/build.gradle.kts new file mode 100644 index 0000000000..0a39730199 --- /dev/null +++ b/libraries/pushstore/impl/build.gradle.kts @@ -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. + */ + +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.push.pushstore.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.dagger) + implementation(projects.libraries.architecture) + implementation(projects.libraries.core) + implementation(projects.libraries.pushstore.api) + implementation(projects.libraries.sessionStorage.api) + implementation(libs.androidx.corektx) + implementation(libs.androidx.datastore.preferences) +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt similarity index 78% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt rename to libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt index 0323713de7..159c1cb892 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt @@ -14,28 +14,32 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.userpushstore +package io.element.android.libraries.pushstore.impl import android.content.Context +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.SingleIn +import io.element.android.libraries.pushstore.api.UserPushStore +import io.element.android.libraries.pushstore.api.UserPushStoreFactory 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( +@ContributesBinding(AppScope::class, boundType = UserPushStoreFactory::class) +class DefaultUserPushStoreFactory @Inject constructor( @ApplicationContext private val context: Context, private val sessionObserver: SessionObserver, -) : SessionListener { +) : UserPushStoreFactory, 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 { + override fun create(userId: String): UserPushStore { return cache.getOrPut(userId) { UserPushStoreDataStore( context = context, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt similarity index 91% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreDataStore.kt rename to libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt index 6f25599e54..8b37056768 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.userpushstore +package io.element.android.libraries.pushstore.impl import android.content.Context import androidx.datastore.core.DataStore @@ -22,6 +22,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import io.element.android.libraries.pushstore.api.UserPushStore import kotlinx.coroutines.flow.first /** @@ -35,8 +36,8 @@ class UserPushStoreDataStore( 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 getNotificationMethod(): String? { + return context.dataStore.data.first()[notificationMethod] } override suspend fun setNotificationMethod(value: String) { diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 9f0cf92099..368747961d 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -75,6 +75,12 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:core")) implementation(project(":libraries:permissions:impl")) implementation(project(":libraries:push:impl")) + implementation(project(":libraries:push:impl")) + // Comment to not include firebase in the project + implementation(project(":libraries:pushproviders:firebase")) + // Comment to not include unified push in the project + // implementation(project(":libraries:pushproviders:unifiedpush")) + implementation(project(":libraries:pushstore:impl")) implementation(project(":libraries:architecture")) implementation(project(":libraries:dateformatter:impl")) implementation(project(":libraries:di")) From 05a8ca0eec89ef1cf389c1e50ccf8546f7aa5c6d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 21:57:55 +0200 Subject: [PATCH 077/119] Sort provider by index --- .../element/android/libraries/push/impl/DefaultPushService.kt | 3 +-- .../android/libraries/push/providers/api/PushProvider.kt | 4 ++++ .../libraries/push/providers/firebase/FirebasePushProvider.kt | 1 + .../push/providers/unifiedpush/UnifiedPushProvider.kt | 4 +++- 4 files changed, 9 insertions(+), 3 deletions(-) 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 9bf4afa7ea..1d9397d9a9 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 @@ -35,8 +35,7 @@ class DefaultPushService @Inject constructor( } override fun getAvailablePushProviders(): List { - // TODO Sort by priority? - return pushProviders.toList() + return pushProviders.sortedBy { it.index } } override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) { diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt index 2b90c3d5b3..6476e4f815 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt @@ -22,6 +22,10 @@ import io.element.android.libraries.matrix.api.MatrixClient * This is the main API for this module */ interface PushProvider { + /** + * Allow to sort provider, from lower index to higher index + */ + val index: Int fun getDistributorNames(): List suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt index 855762e5ab..0e96fc4a26 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt @@ -29,6 +29,7 @@ class FirebasePushProvider @Inject constructor( private val googleFcmHelper: GoogleFcmHelper, private val pusherSubscriber: PusherSubscriber, ) : PushProvider { + override val index = 0 override fun getDistributorNames(): List { // Must return an non-empty list for now diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt index a37e9da5e2..dcf629fe98 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt @@ -20,7 +20,9 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.providers.api.PushProvider import javax.inject.Inject -class UnifiedPushProvider @Inject constructor(): PushProvider { +class UnifiedPushProvider @Inject constructor() : PushProvider { + override val index = 1 + override fun getDistributorNames(): List { TODO("Not yet implemented") } From 586d1a076c13d5f1e4e831629894a2be37b953a4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 22:08:57 +0200 Subject: [PATCH 078/119] Split GoogleFcmHelper --- .../push/providers/api/PushProvider.kt | 5 + ...etPusher.kt => FirebaseNewTokenHandler.kt} | 12 ++- .../firebase/FirebasePushProvider.kt | 9 +- .../push/providers/firebase/FirebaseStore.kt | 43 ++++++++ .../firebase/FirebaseTroubleshooter.kt | 79 +++++++++++++++ .../providers/firebase/GoogleFcmHelper.kt | 98 ------------------- .../VectorFirebaseMessagingService.kt | 6 +- .../unifiedpush/UnifiedPushProvider.kt | 4 + 8 files changed, 148 insertions(+), 108 deletions(-) rename libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/{FirebaseSetPusher.kt => FirebaseNewTokenHandler.kt} (84%) create mode 100644 libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseStore.kt create mode 100644 libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseTroubleshooter.kt delete mode 100644 libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/GoogleFcmHelper.kt diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt index 6476e4f815..854ae43522 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt @@ -28,4 +28,9 @@ interface PushProvider { val index: Int fun getDistributorNames(): List suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) + + /** + * Attempt to troubleshoot the push provider + */ + suspend fun troubleshoot(): Result } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseSetPusher.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt similarity index 84% rename from libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseSetPusher.kt rename to libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt index ea6937551a..01b93f3d95 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseSetPusher.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt @@ -26,16 +26,20 @@ import io.element.android.libraries.sessionstorage.api.toUserList import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("FirebaseSetPusher") +private val loggerTag = LoggerTag("FirebaseNewTokenHandler") -// TODO Rename -class FirebaseSetPusher @Inject constructor( +/** + * Handle new token receive from Firebase. Will update all the sessions which are using Firebase as a push provider. + */ +class FirebaseNewTokenHandler @Inject constructor( private val pusherSubscriber: PusherSubscriber, private val sessionStore: SessionStore, private val userPushStoreFactory: UserPushStoreFactory, private val matrixAuthenticationService: MatrixAuthenticationService, + private val firebaseStore: FirebaseStore, ) { - suspend fun onNewFirebaseToken(firebaseToken: String) { + suspend fun handle(firebaseToken: String) { + firebaseStore.storeFcmToken(firebaseToken) // Register the pusher for all the sessions sessionStore.getAllSessions().toUserList().forEach { userId -> val userDataStore = userPushStoreFactory.create(userId) diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt index 0e96fc4a26..86042ae4e3 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt @@ -26,7 +26,8 @@ import javax.inject.Inject private val loggerTag = LoggerTag("FirebasePushProvider") class FirebasePushProvider @Inject constructor( - private val googleFcmHelper: GoogleFcmHelper, + private val firebaseStore: FirebaseStore, + private val firebaseTroubleshooter: FirebaseTroubleshooter, private val pusherSubscriber: PusherSubscriber, ) : PushProvider { override val index = 0 @@ -37,9 +38,13 @@ class FirebasePushProvider @Inject constructor( } override suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) { - val pushKey = googleFcmHelper.getFcmToken() ?: return Unit.also { + val pushKey = firebaseStore.getFcmToken() ?: return Unit.also { Timber.tag(loggerTag.value).w("Unable to register pusher, Firebase token is not known.") } pusherSubscriber.registerPusher(matrixClient, pushKey, FirebaseConfig.pusher_http_url) } + + override suspend fun troubleshoot(): Result { + return firebaseTroubleshooter.troubleshoot() + } } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseStore.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseStore.kt new file mode 100644 index 0000000000..f25ce08bc7 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseStore.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. + */ + +package io.element.android.libraries.push.providers.firebase + +import android.content.SharedPreferences +import androidx.core.content.edit +import io.element.android.libraries.di.DefaultPreferences +import javax.inject.Inject + +/** + * This class store the Firebase token in SharedPrefs. + */ +class FirebaseStore @Inject constructor( + @DefaultPreferences private val sharedPrefs: SharedPreferences, +) { + fun getFcmToken(): String? { + return sharedPrefs.getString(PREFS_KEY_FCM_TOKEN, null) + } + + fun storeFcmToken(token: String?) { + sharedPrefs.edit { + putString(PREFS_KEY_FCM_TOKEN, token) + } + } + + companion object { + private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" + } +} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseTroubleshooter.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseTroubleshooter.kt new file mode 100644 index 0000000000..9fb9b5708d --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseTroubleshooter.kt @@ -0,0 +1,79 @@ +/* + * 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.providers.firebase + +import android.content.Context +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.firebase.messaging.FirebaseMessaging +import io.element.android.libraries.di.ApplicationContext +import timber.log.Timber +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +/** + * This class force retrieving and storage of the Firebase token. + */ +class FirebaseTroubleshooter @Inject constructor( + @ApplicationContext private val context: Context, + private val newTokenHandler: FirebaseNewTokenHandler, +) { + suspend fun troubleshoot(): Result { + return runCatching { + val token = retrievedFirebaseToken() + newTokenHandler.handle(token) + } + } + + private suspend fun retrievedFirebaseToken(): String { + return suspendCoroutine { continuation -> + // '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 -> + continuation.resume(token) + } + .addOnFailureListener { e -> + Timber.e(e, "## retrievedFirebaseToken() : failed") + continuation.resumeWithException(e) + } + } catch (e: Throwable) { + Timber.e(e, "## retrievedFirebaseToken() : failed") + continuation.resumeWithException(e) + } + } else { + val e = Exception("No valid Google Play Services found. Cannot use FCM.") + Timber.e(e) + continuation.resumeWithException(e) + } + } + } + + /** + * 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 + } +} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/GoogleFcmHelper.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/GoogleFcmHelper.kt deleted file mode 100644 index 0772ac0603..0000000000 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/GoogleFcmHelper.kt +++ /dev/null @@ -1,98 +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.push.providers.firebase - -import android.content.Context -import android.content.SharedPreferences -import androidx.core.content.edit -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability -import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.di.DefaultPreferences -import javax.inject.Inject - -/** - * This class store the FCM token in SharedPrefs and ensure this token is retrieved. - * It has an alter ego in the fdroid variant. - */ -// TODO Rename to store? -class GoogleFcmHelper @Inject constructor( - @ApplicationContext private val context: Context, - @DefaultPreferences private val sharedPrefs: SharedPreferences, -) { - fun getFcmToken(): String? { - return sharedPrefs.getString(PREFS_KEY_FCM_TOKEN, null) - } - - 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) { - runBlocking {// TODO - pushersManager.enqueueRegisterPusherWithFcmKey(token) - } - } - } - .addOnFailureListener { e -> - Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") - } - } catch (e: Throwable) { - Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") - } - } else { - Toast.makeText(context, R.string.push_no_valid_google_play_services_apk_android, 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/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt index 02bb84c541..35434ceb2e 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt @@ -30,10 +30,9 @@ import javax.inject.Inject private val loggerTag = LoggerTag("Firebase") class VectorFirebaseMessagingService : FirebaseMessagingService() { - @Inject lateinit var firebaseSetPusher: FirebaseSetPusher + @Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler @Inject lateinit var pushParser: FirebasePushParser @Inject lateinit var pushHandler: PushHandler - @Inject lateinit var googleFcmHelper: GoogleFcmHelper private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -44,9 +43,8 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { override fun onNewToken(token: String) { Timber.tag(loggerTag.value).d("New Firebase token") - googleFcmHelper.storeFcmToken(token) coroutineScope.launch { - firebaseSetPusher.onNewFirebaseToken(token) + firebaseNewTokenHandler.handle(token) } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt index dcf629fe98..e6d402b8d2 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt @@ -30,4 +30,8 @@ class UnifiedPushProvider @Inject constructor() : PushProvider { override suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) { TODO("Not yet implemented") } + + override suspend fun troubleshoot(): Result { + TODO("Not yet implemented") + } } From 1f87e10376f18b4d7218cf9ede94a5d37bc7f2d4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 8 Apr 2023 00:23:30 +0200 Subject: [PATCH 079/119] Cleanup, Firebase dep. is not necessary here. --- app/build.gradle.kts | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0b8833efbb..bfd69aea3d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -225,9 +225,6 @@ 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) From adfcd61287dd58d0ec29cf0a07a3f2a51e4266ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 8 Apr 2023 00:28:13 +0200 Subject: [PATCH 080/119] Safer code --- .../io/element/android/appnav/loggedin/LoggedInPresenter.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 552420abf8..f548771717 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 @@ -48,7 +48,8 @@ class LoggedInPresenter @Inject constructor( // Ensure pusher is registered // TODO Register with Firebase for now val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect - pushService.registerWith(matrixClient, pushProvider, pushProvider.getDistributorNames().first()) + val distributor = pushProvider.getDistributorNames().firstOrNull() ?: return@LaunchedEffect + pushService.registerWith(matrixClient, pushProvider, distributor) } val permissionsState = postNotificationPermissionsPresenter.present() From 95bafe4059c936fea6b48515de9f3e477f50878c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Apr 2023 11:19:44 +0200 Subject: [PATCH 081/119] UnifiedPush WIP --- app/build.gradle.kts | 1 + .../appnav/loggedin/LoggedInPresenter.kt | 6 +- .../appnav/loggedin/LoggedInPresenterTest.kt | 3 +- .../matrix/api/pusher/PushersService.kt | 1 + .../matrix/impl/pushers/RustPushersService.kt | 5 + .../matrix/test/pushers/FakePushersService.kt | 1 + .../android/libraries/push/api/PushService.kt | 3 +- .../libraries/push/impl/DefaultPushService.kt | 23 ++- .../libraries/push/impl/PushersManager.kt | 5 +- .../push/providers/api/Distributor.kt | 22 +++ .../push/providers/api/PushProvider.kt | 19 ++- .../push/providers/api/PusherSubscriber.kt | 1 + .../pushproviders/firebase/build.gradle.kts | 2 +- .../push/providers/firebase/FirebaseConfig.kt | 3 +- .../firebase/FirebaseNewTokenHandler.kt | 2 +- .../firebase/FirebasePushProvider.kt | 18 ++- .../unifiedpush/build.gradle.kts | 7 + .../unifiedpush/PushDataUnifiedPush.kt | 4 +- .../unifiedpush/RegisterUnifiedPushUseCase.kt | 29 ++-- .../unifiedpush/UnifiedPushConfig.kt | 3 +- .../unifiedpush/UnifiedPushHelper.kt | 138 +++--------------- .../UnifiedPushNewGatewayHandler.kt | 53 +++++++ .../unifiedpush/UnifiedPushParser.kt | 4 +- .../unifiedpush/UnifiedPushProvider.kt | 34 ++++- .../UnregisterUnifiedPushUseCase.kt | 18 ++- .../VectorUnifiedPushMessagingReceiver.kt | 64 ++++---- .../unifiedpush/network/DiscoveryResponse.kt | 25 ++++ .../network/DiscoveryUnifiedPush.kt | 25 ++++ .../unifiedpush/network/UnifiedPushApi.kt | 24 +++ .../unifiedpush/UnifiedPushParserTest.kt | 29 ++-- .../libraries/pushstore/api/UserPushStore.kt | 7 +- .../pushstore/impl/UserPushStoreDataStore.kt | 10 +- .../kotlin/extension/DependencyHandleScope.kt | 2 +- 33 files changed, 376 insertions(+), 215 deletions(-) create mode 100644 libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/Distributor.kt create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryResponse.kt create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryUnifiedPush.kt create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/UnifiedPushApi.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bfd69aea3d..74ebfa89d4 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") + // TODO Move the plugin to the firebase module? id("com.google.gms.google-services") } 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 f548771717..a798f95bae 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,9 +46,9 @@ class LoggedInPresenter @Inject constructor( override fun present(): LoggedInState { LaunchedEffect(Unit) { // Ensure pusher is registered - // TODO Register with Firebase for now - val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect - val distributor = pushProvider.getDistributorNames().firstOrNull() ?: return@LaunchedEffect + // TODO Manually select push provider for now + val pushProvider = pushService.getAvailablePushProviders().find { it.name == "UnifiedPush" } ?: return@LaunchedEffect + val distributor = pushProvider.getDistributors().first() pushService.registerWith(matrixClient, pushProvider, distributor) } 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 index 595f4850b2..71d303150b 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -27,6 +27,7 @@ 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 io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -60,7 +61,7 @@ class LoggedInPresenterTest { return emptyList() } - override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) { + override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) { } override suspend fun testPush() { 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 ef2291f8ce..71a642965f 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 @@ -18,4 +18,5 @@ package io.element.android.libraries.matrix.api.pusher interface PushersService { suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result + suspend fun unsetHttpPusher(): Result } 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 4eaafef12d..60ca4df311 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 @@ -53,4 +53,9 @@ class RustPushersService( } } } + + override suspend fun unsetHttpPusher(): Result { + // TODO Missing client API. We need to set the pusher with Kind == null, but we do not have access to this field from the SDK. + return Result.success(Unit) + } } 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 77087d132f..6ff7e4a20b 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 @@ -21,4 +21,5 @@ import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData class FakePushersService : PushersService { override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Result.success(Unit) + override suspend fun unsetHttpPusher(): Result = Result.success(Unit) } 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 7eeca6e0a3..83504e7a8a 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 @@ -17,6 +17,7 @@ package io.element.android.libraries.push.api import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider interface PushService { @@ -30,7 +31,7 @@ interface PushService { * * The method has effect only if the [PushProvider] is different than the current one. */ - suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) + suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) // TODO Move away 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 1d9397d9a9..d1e266357b 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,14 +20,19 @@ 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.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager +import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider +import io.element.android.libraries.pushstore.api.UserPushStoreFactory import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultPushService @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val pushersManager: PushersManager, + private val pushClientSecret: PushClientSecret, + private val userPushStoreFactory: UserPushStoreFactory, private val pushProviders: Set<@JvmSuppressWildcards PushProvider>, ) : PushService { override fun notificationStyleChanged() { @@ -38,10 +43,22 @@ class DefaultPushService @Inject constructor( return pushProviders.sortedBy { it.index } } - override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) { - // TODO Get current push provider, compare with provided one, then unregister and register if different, and store change + /** + * Get current push provider, compare with provided one, then unregister and register if different, and store change + */ + override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) { + val userPushStore = userPushStoreFactory.create(matrixClient.sessionId.value) + val currentPushProviderName = userPushStore.getPushProviderName() + if (currentPushProviderName != pushProvider.name) { + // Unregister previous one if any + pushProviders.find { it.name == currentPushProviderName }?.unregister(matrixClient) + } - pushProvider.registerWith(matrixClient, distributorName) + val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId) + pushProvider.registerWith(matrixClient, distributor, clientSecret) + + // Store new value + userPushStore.setPushProviderName(pushProvider.name) } 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 532a904c82..eb3157adff 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 @@ -148,9 +148,8 @@ class PushersManager @Inject constructor( // currentSession.pushersService().removeEmailPusher(email) } - suspend fun unregisterPusher(pushKey: String) { - // val currentSession = activeSessionHolder.getSafeActiveSession() ?: return - // currentSession.pushersService().removeHttpPusher(pushKey, PushConfig.pusher_app_id) + override suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) { + matrixClient.pushersService().unsetHttpPusher() } companion object { diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/Distributor.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/Distributor.kt new file mode 100644 index 0000000000..3d4d0add28 --- /dev/null +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/Distributor.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.providers.api + +data class Distributor( + val value: String, + val name: String, +) diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt index 854ae43522..92246851ec 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt @@ -26,8 +26,23 @@ interface PushProvider { * Allow to sort provider, from lower index to higher index */ val index: Int - fun getDistributorNames(): List - suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) + + /** + * User friendly name. + */ + val name: String + + fun getDistributors(): List + + /** + * Register the pusher to the homeserver + */ + suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) + + /** + * Unregister the pusher + */ + suspend fun unregister(matrixClient: MatrixClient) /** * Attempt to troubleshoot the push provider diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt index 805244e0ed..0bf0f949f3 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt @@ -20,4 +20,5 @@ import io.element.android.libraries.matrix.api.MatrixClient interface PusherSubscriber { suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) + suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) } diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index 65360d5465..eabc70305b 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -40,7 +40,7 @@ dependencies { implementation(projects.libraries.pushproviders.api) implementation(platform(libs.google.firebase.bom)) - implementation("com.google.firebase:firebase-messaging-ktx") + api("com.google.firebase:firebase-messaging-ktx") testImplementation(libs.test.junit) testImplementation(libs.test.truth) diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt index 27463825d6..bf35a1b18a 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt @@ -23,5 +23,6 @@ object FirebaseConfig { */ const val pusher_http_url: String = "https://matrix.org/_matrix/push/v1/notify" - const val internalName = "NOTIFICATION_METHOD_FIREBASE" + const val index = 0 + const val name = "Firebase" } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt index 01b93f3d95..2c987c75e1 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt @@ -43,7 +43,7 @@ class FirebaseNewTokenHandler @Inject constructor( // Register the pusher for all the sessions sessionStore.getAllSessions().toUserList().forEach { userId -> val userDataStore = userPushStoreFactory.create(userId) - if (userDataStore.getNotificationMethod() == FirebaseConfig.internalName) { + if (userDataStore.getPushProviderName() == FirebaseConfig.name) { matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client -> pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.pusher_http_url) } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt index 86042ae4e3..f30e031bab 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.push.providers.firebase import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider import io.element.android.libraries.push.providers.api.PusherSubscriber import timber.log.Timber @@ -30,20 +31,27 @@ class FirebasePushProvider @Inject constructor( private val firebaseTroubleshooter: FirebaseTroubleshooter, private val pusherSubscriber: PusherSubscriber, ) : PushProvider { - override val index = 0 + override val index = FirebaseConfig.index + override val name = FirebaseConfig.name - override fun getDistributorNames(): List { - // Must return an non-empty list for now - return listOf("unused") + override fun getDistributors(): List { + return listOf(Distributor("Firebase", "Firebase")) } - override suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) { + override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) { val pushKey = firebaseStore.getFcmToken() ?: return Unit.also { Timber.tag(loggerTag.value).w("Unable to register pusher, Firebase token is not known.") } pusherSubscriber.registerPusher(matrixClient, pushKey, FirebaseConfig.pusher_http_url) } + override suspend fun unregister(matrixClient: MatrixClient) { + val pushKey = firebaseStore.getFcmToken() ?: return Unit.also { + Timber.tag(loggerTag.value).w("Unable to unregister pusher, Firebase token is not known.") + } + pusherSubscriber.unregisterPusher(matrixClient, pushKey, FirebaseConfig.pusher_http_url) + } + override suspend fun troubleshoot(): Result { return firebaseTroubleshooter.troubleshoot() } diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts index 6817d0aad6..7dcd773c25 100644 --- a/libraries/pushproviders/unifiedpush/build.gradle.kts +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -30,14 +30,21 @@ anvil { dependencies { implementation(libs.dagger) + implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.pushstore.api) implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.architecture) implementation(projects.libraries.core) implementation(projects.services.toolbox.api) + implementation(projects.libraries.network) + implementation(platform(libs.network.okhttp.bom)) + implementation("com.squareup.okhttp3:okhttp") + implementation(libs.network.retrofit) + implementation(libs.serialization.json) // UnifiedPush library diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt index 6d5ecb1db3..618a3c989f 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt @@ -55,13 +55,13 @@ data class PushDataUnifiedPushCounts( @SerialName("unread") val unread: Int? = null ) -fun PushDataUnifiedPush.toPushData(): PushData? { +fun PushDataUnifiedPush.toPushData(clientSecret: String): PushData? { val safeEventId = notification?.eventId?.asEventId() ?: return null val safeRoomId = notification.roomId?.asRoomId() ?: return null return PushData( eventId = safeEventId, roomId = safeRoomId, unread = notification.counts?.unread, - clientSecret = null // TODO EAx check how client secret will be sent through UnifiedPush + clientSecret = clientSecret ) } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt index a80d9ba865..813f8e97ca 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt @@ -18,45 +18,56 @@ package io.element.android.libraries.push.providers.unifiedpush import android.content.Context import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.providers.api.Distributor +import io.element.android.libraries.push.providers.api.PusherSubscriber import org.unifiedpush.android.connector.UnifiedPush import javax.inject.Inject class RegisterUnifiedPushUseCase @Inject constructor( @ApplicationContext private val context: Context, + private val pusherSubscriber: PusherSubscriber, + private val unifiedPushStore: UnifiedPushStore, ) { sealed interface RegisterUnifiedPushResult { object Success : RegisterUnifiedPushResult object NeedToAskUserForDistributor : RegisterUnifiedPushResult + object Error : RegisterUnifiedPushResult } - fun execute(distributor: String = ""): RegisterUnifiedPushResult { - if (distributor.isNotEmpty()) { - saveAndRegisterApp(distributor) + suspend fun execute(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String): RegisterUnifiedPushResult { + val distributorValue = distributor.value + if (distributorValue.isNotEmpty()) { + saveAndRegisterApp(distributorValue, clientSecret) + val endpoint = unifiedPushStore.getEndpoint() ?: return RegisterUnifiedPushResult.Error + val gateway = unifiedPushStore.getPushGateway() ?: return RegisterUnifiedPushResult.Error + pusherSubscriber.registerPusher(matrixClient, endpoint, gateway) return RegisterUnifiedPushResult.Success } + // TODO Below should never happen? if (UnifiedPush.getDistributor(context).isNotEmpty()) { - registerApp() + registerApp(clientSecret) return RegisterUnifiedPushResult.Success } val distributors = UnifiedPush.getDistributors(context) return if (distributors.size == 1) { - saveAndRegisterApp(distributors.first()) + saveAndRegisterApp(distributors.first(), clientSecret) RegisterUnifiedPushResult.Success } else { RegisterUnifiedPushResult.NeedToAskUserForDistributor } } - private fun saveAndRegisterApp(distributor: String) { + private fun saveAndRegisterApp(distributor: String, clientSecret: String) { UnifiedPush.saveDistributor(context, distributor) - registerApp() + registerApp(clientSecret) } - private fun registerApp() { - UnifiedPush.registerApp(context) + private fun registerApp(clientSecret: String) { + UnifiedPush.registerApp(context = context, instance = clientSecret) } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt index 73c31f430c..21b4ca9a76 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt @@ -23,5 +23,6 @@ object UnifiedPushConfig { */ const val default_push_gateway_http_url: String = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify" - const val internalName = "NOTIFICATION_METHOD_UNIFIEDPUSH" + const val index = 1 + const val name = "UnifiedPush" } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt index dce17015b7..6c17c16c67 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt @@ -17,11 +17,13 @@ package io.element.android.libraries.push.providers.unifiedpush import android.content.Context +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.network.RetrofitFactory +import io.element.android.libraries.push.providers.unifiedpush.network.UnifiedPushApi import io.element.android.services.toolbox.api.strings.StringProvider -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import org.unifiedpush.android.connector.UnifiedPush +import kotlinx.coroutines.withContext import timber.log.Timber import java.net.URL import javax.inject.Inject @@ -30,132 +32,34 @@ class UnifiedPushHelper @Inject constructor( @ApplicationContext private val context: Context, private val unifiedPushStore: UnifiedPushStore, private val stringProvider: StringProvider, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: CoroutineDispatchers, ) { - - /* TODO EAx - @MainThread - fun showSelectDistributorDialog( - context: Context, - onDistributorSelected: (String) -> Unit, - ) { - val internalDistributorName = stringProvider.getString( - if (fcmHelper.isFirebaseAvailable()) { - R.string.push_distributor_firebase_android - } else { - R.string.push_distributor_background_sync_android - } - ) - - val distributors = UnifiedPush.getDistributors(context) - val distributorsName = distributors.map { - if (it == context.packageName) { - internalDistributorName - } else { - context.getApplicationLabel(it) - } - } - - MaterialAlertDialogBuilder(context) - .setTitle(stringProvider.getString(R.string.push_choose_distributor_dialog_title_android)) - .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 + suspend fun storeCustomOrDefaultGateway(endpoint: String) { + val gateway = UnifiedPushConfig.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 + withContext(coroutineDispatchers.io) { + val api = retrofitFactory.create("${parsed.protocol}://${parsed.host}") + .create(UnifiedPushApi::class.java) + tryOrNull { api.discover() } + ?.let { discoveryResponse -> + if (discoveryResponse.unifiedpush.gateway == "matrix") { + Timber.d("Using custom gateway") + unifiedPushStore.storePushGateway(custom) + } } - } + } + 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 { - TODO() - /* - return when { - 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)) - } - - */ - } - - fun isEmbeddedDistributor(): Boolean { - TODO() - //return isInternalDistributor() && fcmHelper.isFirebaseAvailable() - } - - fun isBackgroundSync(): Boolean { - TODO() - //return isInternalDistributor() && !fcmHelper.isFirebaseAvailable() - } - - private fun isInternalDistributor(): Boolean { - return UnifiedPush.getDistributor(context).isEmpty() || - UnifiedPush.getDistributor(context) == context.packageName - } + private fun isEmbeddedDistributor() = false fun getPrivacyFriendlyUpEndpoint(): String? { val endpoint = getEndpointOrToken() diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt new file mode 100644 index 0000000000..b02fab04ea --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt @@ -0,0 +1,53 @@ +/* + * 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.providers.unifiedpush + +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.push.providers.api.PusherSubscriber +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.api.toUserList +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("UnifiedPushNewGatewayHandler") + +/** + * Handle new endpoint received from UnifiedPush. Will update all the sessions which are using UnifiedPush as a push provider. + */ +class UnifiedPushNewGatewayHandler @Inject constructor( + private val pusherSubscriber: PusherSubscriber, + private val sessionStore: SessionStore, + private val userPushStoreFactory: UserPushStoreFactory, + private val matrixAuthenticationService: MatrixAuthenticationService, +) { + suspend fun handle(endpoint: String, pushGateway: String) { + // Register the pusher for all the sessions which are using UnifiedPush. + sessionStore.getAllSessions().toUserList().forEach { userId -> + val userDataStore = userPushStoreFactory.create(userId) + if (userDataStore.getPushProviderName() == UnifiedPushConfig.name) { + matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client -> + pusherSubscriber.registerPusher(client, endpoint, pushGateway) + } + } else { + Timber.tag(loggerTag.value).d("This session is not using UnifiedPush pusher") + } + } + } +} diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt index 881862d473..6169e1f8eb 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt @@ -25,7 +25,7 @@ import javax.inject.Inject class UnifiedPushParser @Inject constructor() { private val json by lazy { Json { ignoreUnknownKeys = true } } - fun parse(message: ByteArray): PushData? { - return tryOrNull { json.decodeFromString(String(message)) }?.toPushData() + fun parse(message: ByteArray, clientSecret: String): PushData? { + return tryOrNull { json.decodeFromString(String(message)) }?.toPushData(clientSecret) } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt index e6d402b8d2..c12c54c0a3 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt @@ -16,19 +16,41 @@ package io.element.android.libraries.push.providers.unifiedpush +import android.content.Context +import io.element.android.libraries.androidutils.system.getApplicationLabel +import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider +import org.unifiedpush.android.connector.UnifiedPush import javax.inject.Inject -class UnifiedPushProvider @Inject constructor() : PushProvider { - override val index = 1 +class UnifiedPushProvider @Inject constructor( + @ApplicationContext private val context: Context, + private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, + private val unRegisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, +) : PushProvider { + override val index = UnifiedPushConfig.index + override val name = UnifiedPushConfig.name - override fun getDistributorNames(): List { - TODO("Not yet implemented") + override fun getDistributors(): List { + val distributors = UnifiedPush.getDistributors(context) + return distributors.mapNotNull { + if (it == context.packageName) { + // Exclude self + null + } else { + Distributor(it, context.getApplicationLabel(it)) + } + } } - override suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) { - TODO("Not yet implemented") + override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) { + registerUnifiedPushUseCase.execute(matrixClient, distributor, clientSecret) + } + + override suspend fun unregister(matrixClient: MatrixClient) { + unRegisterUnifiedPushUseCase.execute() } override suspend fun troubleshoot(): Result { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt index b7cd592951..750eb95f83 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt @@ -16,21 +16,26 @@ package io.element.android.libraries.push.providers.unifiedpush -/* +import android.content.Context +import io.element.android.libraries.di.ApplicationContext +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 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) + 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) + // TODO pushersManager?.unregisterPusher(it) } } catch (e: Exception) { Timber.d(e, "Probably unregistering a non existing pusher") @@ -40,4 +45,3 @@ class UnregisterUnifiedPushUseCase @Inject constructor( UnifiedPush.unregisterApp(context) } } - */ diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index d745df6e87..0736badbc2 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt @@ -21,6 +21,7 @@ import android.content.Intent import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.push.providers.api.PushHandler +import io.element.android.libraries.push.providers.api.PusherSubscriber import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -32,22 +33,24 @@ private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver") class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { @Inject lateinit var pushParser: UnifiedPushParser - // @Inject lateinit var pushDataStore: PushDataStore + + // @Inject lateinit var pushDataStore: PushDataStore @Inject lateinit var pushHandler: PushHandler @Inject lateinit var guardServiceStarter: GuardServiceStarter -// @Inject lateinit var unifiedPushStore: UnifiedPushStore -// @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + @Inject lateinit var unifiedPushStore: UnifiedPushStore + @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + @Inject lateinit var pusherSubscriber: PusherSubscriber + @Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler private val coroutineScope = CoroutineScope(SupervisorJob()) override fun onReceive(context: Context, intent: Intent) { - super.onReceive(context, intent) - // Inject context.applicationContext.bindings().inject(this) + super.onReceive(context, intent) } /** - * Called when message is received. + * Called when message is received. The message contains the full POST body of the push message. * * @param context the Android context * @param message the message @@ -56,7 +59,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { override fun onMessage(context: Context, message: ByteArray, instance: String) { Timber.tag(loggerTag.value).d("New message") coroutineScope.launch { - val pushData = pushParser.parse(message) + val pushData = pushParser.parse(message, instance) if (pushData == null) { Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush") } else { @@ -65,36 +68,36 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { } } + /** + * Called when a new endpoint is to be used for sending push messages. + * You should send the endpoint to your application server and sync for missing notifications. + * TODO use [instance] for multi-account + */ override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { - TODO() - /* 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 { - coroutineScope.launch { - pushersManager.onNewUnifiedPushEndpoint(endpoint, it) - } - } - } + // 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 { pushGateway -> + newGatewayHandler.handle(endpoint, pushGateway) } - } else { - Timber.tag(loggerTag.value).i("onNewEndpoint: skipped") } + } else { + Timber.tag(loggerTag.value).i("onNewEndpoint: skipped") } - val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED - pushDataStore.setFdroidSyncBackgroundMode(mode) + //val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED + //pushDataStore.setFdroidSyncBackgroundMode(mode) guardServiceStarter.stop() - */ } + /** + * Called when the registration is not possible, eg. no network. + */ override fun onRegistrationFailed(context: Context, instance: String) { - TODO() + Timber.tag(loggerTag.value).e("onRegistrationFailed for $instance") /* Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show() val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME @@ -103,10 +106,13 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { */ } + /** + * Called when this application is unregistered from receiving push messages. + */ override fun onUnregistered(context: Context, instance: String) { + Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered") TODO() /* - Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered") val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME pushDataStore.setFdroidSyncBackgroundMode(mode) guardServiceStarter.start() diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryResponse.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryResponse.kt new file mode 100644 index 0000000000..b961da1285 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryResponse.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.providers.unifiedpush.network + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DiscoveryResponse( + @SerialName("unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush() +) diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryUnifiedPush.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryUnifiedPush.kt new file mode 100644 index 0000000000..b4c7345fd7 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryUnifiedPush.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.providers.unifiedpush.network + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DiscoveryUnifiedPush( + @SerialName("gateway") val gateway: String = "" +) diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/UnifiedPushApi.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/UnifiedPushApi.kt new file mode 100644 index 0000000000..e384b8353b --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/UnifiedPushApi.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.providers.unifiedpush.network + +import retrofit2.http.GET + +interface UnifiedPushApi { + @GET("_matrix/push/v1/notify") + suspend fun discover(): DiscoveryResponse +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt index 6b5c0db62f..b18be39e07 100644 --- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt @@ -20,56 +20,65 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.push.providers.api.PushData +import org.junit.Assert.assertThrows import org.junit.Test class UnifiedPushParserTest { + private val aClientSecret = "a-client-secret" private val validData = PushData( eventId = AN_EVENT_ID, roomId = A_ROOM_ID, unread = 1, - // TODO handle client secret here. - clientSecret = null + clientSecret = aClientSecret ) @Test fun `test edge cases UnifiedPush`() { val pushParser = UnifiedPushParser() // Empty string - assertThat(pushParser.parse("".toByteArray())).isNull() + assertThat(pushParser.parse("".toByteArray(), aClientSecret)).isNull() // Empty Json - assertThat(pushParser.parse("{}".toByteArray())).isNull() + assertThat(pushParser.parse("{}".toByteArray(), aClientSecret)).isNull() // Bad Json - assertThat(pushParser.parse("ABC".toByteArray())).isNull() + assertThat(pushParser.parse("ABC".toByteArray(), aClientSecret)).isNull() } @Test fun `test UnifiedPush format`() { val pushParser = UnifiedPushParser() - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.toByteArray())).isEqualTo(validData) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.toByteArray(), aClientSecret)).isEqualTo(validData) } @Test fun `test empty roomId`() { val pushParser = UnifiedPushParser() - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray())).isNull() + assertThrows(IllegalStateException::class.java) { + pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray(), aClientSecret) + } } @Test fun `test invalid roomId`() { val pushParser = UnifiedPushParser() - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"))).isNull() + assertThrows(IllegalStateException::class.java) { + pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"), aClientSecret) + } } @Test fun `test empty eventId`() { val pushParser = UnifiedPushParser() - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""))).isNull() + assertThrows(IllegalStateException::class.java) { + pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""), aClientSecret) + } } @Test fun `test invalid eventId`() { val pushParser = UnifiedPushParser() - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"))).isNull() + assertThrows(IllegalStateException::class.java) { + pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"), aClientSecret) + } } companion object { diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt index 35ec23f80d..6817199e13 100644 --- a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt @@ -20,12 +20,9 @@ package io.element.android.libraries.pushstore.api * Store data related to push about a user. */ interface UserPushStore { - /** - * [NOTIFICATION_METHOD_FIREBASE] or [NOTIFICATION_METHOD_UNIFIEDPUSH]. - */ - suspend fun getNotificationMethod(): String? + suspend fun getPushProviderName(): String? - suspend fun setNotificationMethod(value: String) + suspend fun setPushProviderName(value: String) suspend fun getCurrentRegisteredPushKey(): String? diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt index 8b37056768..0e4e668eda 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt @@ -33,16 +33,16 @@ class UserPushStoreDataStore( userId: String, ) : UserPushStore { private val Context.dataStore: DataStore by preferencesDataStore(name = "push_store_$userId") - private val notificationMethod = stringPreferencesKey("notificationMethod") + private val pushProviderName = stringPreferencesKey("pushProviderName") private val currentPushKey = stringPreferencesKey("currentPushKey") - override suspend fun getNotificationMethod(): String? { - return context.dataStore.data.first()[notificationMethod] + override suspend fun getPushProviderName(): String? { + return context.dataStore.data.first()[pushProviderName] } - override suspend fun setNotificationMethod(value: String) { + override suspend fun setPushProviderName(value: String) { context.dataStore.edit { - it[notificationMethod] = value + it[pushProviderName] = value } } diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 368747961d..1427269755 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -79,7 +79,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { // Comment to not include firebase in the project implementation(project(":libraries:pushproviders:firebase")) // Comment to not include unified push in the project - // implementation(project(":libraries:pushproviders:unifiedpush")) + implementation(project(":libraries:pushproviders:unifiedpush")) implementation(project(":libraries:pushstore:impl")) implementation(project(":libraries:architecture")) implementation(project(":libraries:dateformatter:impl")) From 80268156b59b7a77efd201e738ef39b168fe54e1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Apr 2023 16:29:31 +0200 Subject: [PATCH 082/119] UnifiedPush WIP --- .../libraries/push/impl/DefaultPushService.kt | 10 +-- .../libraries/push/impl/PushersManager.kt | 4 +- .../push/impl/push/DefaultPushHandler.kt | 2 +- .../push/providers/api/PushProvider.kt | 2 +- .../firebase/FirebaseNewTokenHandler.kt | 20 +++-- .../firebase/FirebasePushProvider.kt | 4 +- .../unifiedpush/RegisterUnifiedPushUseCase.kt | 4 +- .../unifiedpush/UnifiedPushGatewayResolver.kt | 56 ++++++++++++ .../unifiedpush/UnifiedPushHelper.kt | 89 ------------------- .../UnifiedPushNewGatewayHandler.kt | 27 +++--- .../unifiedpush/UnifiedPushProvider.kt | 8 +- .../providers/unifiedpush/UnifiedPushStore.kt | 19 ++-- .../UnregisterUnifiedPushUseCase.kt | 10 +-- .../VectorUnifiedPushMessagingReceiver.kt | 20 ++--- libraries/pushstore/api/build.gradle.kts | 1 + .../pushstore/api/UserPushStoreFactory.kt | 4 +- .../api}/clientsecret/PushClientSecret.kt | 2 +- .../clientsecret/PushClientSecretFactory.kt | 2 +- .../clientsecret/PushClientSecretStore.kt | 2 +- libraries/pushstore/impl/build.gradle.kts | 9 ++ .../impl/DefaultUserPushStoreFactory.kt | 8 +- .../pushstore/impl/UserPushStoreDataStore.kt | 3 +- .../PushClientSecretFactoryImpl.kt | 3 +- .../impl/clientsecret/PushClientSecretImpl.kt | 5 +- .../PushClientSecretStoreDataStore.kt | 3 +- .../FakePushClientSecretFactory.kt | 4 +- .../InMemoryPushClientSecretStore.kt | 3 +- .../clientsecret/PushClientSecretImplTest.kt | 2 +- 28 files changed, 155 insertions(+), 171 deletions(-) create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushGatewayResolver.kt delete mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api}/clientsecret/PushClientSecret.kt (94%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api}/clientsecret/PushClientSecretFactory.kt (91%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api}/clientsecret/PushClientSecretStore.kt (93%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push => pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore}/impl/clientsecret/PushClientSecretFactoryImpl.kt (86%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push => pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore}/impl/clientsecret/PushClientSecretImpl.kt (84%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push => pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore}/impl/clientsecret/PushClientSecretStoreDataStore.kt (94%) rename libraries/{push/impl/src/test/kotlin/io/element/android/libraries/push => pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore}/impl/clientsecret/FakePushClientSecretFactory.kt (85%) rename libraries/{push/impl/src/test/kotlin/io/element/android/libraries/push => pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore}/impl/clientsecret/InMemoryPushClientSecretStore.kt (89%) rename libraries/{push/impl/src/test/kotlin/io/element/android/libraries/push => pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore}/impl/clientsecret/PushClientSecretImplTest.kt (97%) 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 d1e266357b..99af1317f9 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,7 +20,7 @@ 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.clientsecret.PushClientSecret +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider @@ -31,7 +31,6 @@ import javax.inject.Inject class DefaultPushService @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val pushersManager: PushersManager, - private val pushClientSecret: PushClientSecret, private val userPushStoreFactory: UserPushStoreFactory, private val pushProviders: Set<@JvmSuppressWildcards PushProvider>, ) : PushService { @@ -47,16 +46,13 @@ class DefaultPushService @Inject constructor( * Get current push provider, compare with provided one, then unregister and register if different, and store change */ override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) { - val userPushStore = userPushStoreFactory.create(matrixClient.sessionId.value) + val userPushStore = userPushStoreFactory.create(matrixClient.sessionId) val currentPushProviderName = userPushStore.getPushProviderName() if (currentPushProviderName != pushProvider.name) { // Unregister previous one if any pushProviders.find { it.name == currentPushProviderName }?.unregister(matrixClient) } - - val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId) - pushProvider.registerWith(matrixClient, distributor, clientSecret) - + pushProvider.registerWith(matrixClient, distributor) // Store new value userPushStore.setPushProviderName(pushProvider.name) } 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 eb3157adff..33b8ac3507 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 @@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.EventId 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.pushstore.api.clientsecret.PushClientSecret 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.pushgateway.PushGatewayNotifyRequest @@ -79,7 +79,7 @@ class PushersManager @Inject constructor( * Register a pusher to the server if not done yet. */ override suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) { - val userDataStore = userPushStoreFactory.create(matrixClient.sessionId.value) + val userDataStore = userPushStoreFactory.create(matrixClient.sessionId) if (userDataStore.getCurrentRegisteredPushKey() == pushKey) { Timber.tag(loggerTag.value).d("Unnecessary to register again the same pusher") } else { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index 30ee74f3de..7eef95fc67 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -30,7 +30,7 @@ import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService 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.pushstore.api.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 diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt index 92246851ec..ebcdd962d6 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt @@ -37,7 +37,7 @@ interface PushProvider { /** * Register the pusher to the homeserver */ - suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) + suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) /** * Unregister the pusher diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt index 2c987c75e1..f26fcfae25 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt @@ -18,7 +18,7 @@ package io.element.android.libraries.push.providers.firebase import io.element.android.libraries.core.log.logger.LoggerTag 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.asSessionId import io.element.android.libraries.push.providers.api.PusherSubscriber import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.sessionstorage.api.SessionStore @@ -41,15 +41,17 @@ class FirebaseNewTokenHandler @Inject constructor( suspend fun handle(firebaseToken: String) { firebaseStore.storeFcmToken(firebaseToken) // Register the pusher for all the sessions - sessionStore.getAllSessions().toUserList().forEach { userId -> - val userDataStore = userPushStoreFactory.create(userId) - if (userDataStore.getPushProviderName() == FirebaseConfig.name) { - matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client -> - pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.pusher_http_url) + sessionStore.getAllSessions().toUserList() + .map { it.asSessionId() } + .forEach { userId -> + val userDataStore = userPushStoreFactory.create(userId) + if (userDataStore.getPushProviderName() == FirebaseConfig.name) { + matrixAuthenticationService.restoreSession(userId).getOrNull()?.use { client -> + pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.pusher_http_url) + } + } else { + Timber.tag(loggerTag.value).d("This session is not using Firebase pusher") } - } else { - Timber.tag(loggerTag.value).d("This session is not using Firebase pusher") } - } } } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt index f30e031bab..cfddff0bdf 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider import io.element.android.libraries.push.providers.api.PusherSubscriber +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import timber.log.Timber import javax.inject.Inject @@ -30,6 +31,7 @@ class FirebasePushProvider @Inject constructor( private val firebaseStore: FirebaseStore, private val firebaseTroubleshooter: FirebaseTroubleshooter, private val pusherSubscriber: PusherSubscriber, + private val pushClientSecret: PushClientSecret, ) : PushProvider { override val index = FirebaseConfig.index override val name = FirebaseConfig.name @@ -38,7 +40,7 @@ class FirebasePushProvider @Inject constructor( return listOf(Distributor("Firebase", "Firebase")) } - override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) { + override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) { val pushKey = firebaseStore.getFcmToken() ?: return Unit.also { Timber.tag(loggerTag.value).w("Unable to register pusher, Firebase token is not known.") } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt index 813f8e97ca..bff6b06876 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt @@ -40,8 +40,8 @@ class RegisterUnifiedPushUseCase @Inject constructor( val distributorValue = distributor.value if (distributorValue.isNotEmpty()) { saveAndRegisterApp(distributorValue, clientSecret) - val endpoint = unifiedPushStore.getEndpoint() ?: return RegisterUnifiedPushResult.Error - val gateway = unifiedPushStore.getPushGateway() ?: return RegisterUnifiedPushResult.Error + val endpoint = unifiedPushStore.getEndpoint(clientSecret) ?: return RegisterUnifiedPushResult.Error + val gateway = unifiedPushStore.getPushGateway(clientSecret) ?: return RegisterUnifiedPushResult.Error pusherSubscriber.registerPusher(matrixClient, endpoint, gateway) return RegisterUnifiedPushResult.Success } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushGatewayResolver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushGatewayResolver.kt new file mode 100644 index 0000000000..9a1e1785a4 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushGatewayResolver.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.providers.unifiedpush + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.network.RetrofitFactory +import io.element.android.libraries.push.providers.unifiedpush.network.UnifiedPushApi +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.net.URL +import javax.inject.Inject + +class UnifiedPushGatewayResolver @Inject constructor( + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: CoroutineDispatchers, +) { + suspend fun getGateway(endpoint: String): String? { + val gateway = UnifiedPushConfig.default_push_gateway_http_url + val url = URL(endpoint) + val custom = "${url.protocol}://${url.host}/_matrix/push/v1/notify" + Timber.i("Testing $custom") + try { + return withContext(coroutineDispatchers.io) { + val api = retrofitFactory.create("${url.protocol}://${url.host}") + .create(UnifiedPushApi::class.java) + try { + val discoveryResponse = api.discover() + if (discoveryResponse.unifiedpush.gateway == "matrix") { + Timber.d("Using custom gateway") + return@withContext custom + } + } catch (throwable: Throwable) { + Timber.tag("UnifiedPushHelper").e(throwable) + } + return@withContext gateway + } + } catch (e: Throwable) { + Timber.d(e, "Cannot try custom gateway") + } + return gateway + } +} diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt deleted file mode 100644 index 6c17c16c67..0000000000 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt +++ /dev/null @@ -1,89 +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.push.providers.unifiedpush - -import android.content.Context -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.core.data.tryOrNull -import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.network.RetrofitFactory -import io.element.android.libraries.push.providers.unifiedpush.network.UnifiedPushApi -import io.element.android.services.toolbox.api.strings.StringProvider -import kotlinx.coroutines.withContext -import timber.log.Timber -import java.net.URL -import javax.inject.Inject - -class UnifiedPushHelper @Inject constructor( - @ApplicationContext private val context: Context, - private val unifiedPushStore: UnifiedPushStore, - private val stringProvider: StringProvider, - private val retrofitFactory: RetrofitFactory, - private val coroutineDispatchers: CoroutineDispatchers, -) { - suspend fun storeCustomOrDefaultGateway(endpoint: String) { - val gateway = UnifiedPushConfig.default_push_gateway_http_url - val parsed = URL(endpoint) - val custom = "${parsed.protocol}://${parsed.host}/_matrix/push/v1/notify" - Timber.i("Testing $custom") - try { - withContext(coroutineDispatchers.io) { - val api = retrofitFactory.create("${parsed.protocol}://${parsed.host}") - .create(UnifiedPushApi::class.java) - tryOrNull { api.discover() } - ?.let { discoveryResponse -> - if (discoveryResponse.unifiedpush.gateway == "matrix") { - Timber.d("Using custom gateway") - unifiedPushStore.storePushGateway(custom) - } - } - } - return - } catch (e: Throwable) { - Timber.d(e, "Cannot try custom gateway") - } - unifiedPushStore.storePushGateway(gateway) - } - - private fun isEmbeddedDistributor() = false - - 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? { - // TODO - 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/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt index b02fab04ea..3c9833010e 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt @@ -18,11 +18,9 @@ package io.element.android.libraries.push.providers.unifiedpush import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.push.providers.api.PusherSubscriber import io.element.android.libraries.pushstore.api.UserPushStoreFactory -import io.element.android.libraries.sessionstorage.api.SessionStore -import io.element.android.libraries.sessionstorage.api.toUserList +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import timber.log.Timber import javax.inject.Inject @@ -33,21 +31,22 @@ private val loggerTag = LoggerTag("UnifiedPushNewGatewayHandler") */ class UnifiedPushNewGatewayHandler @Inject constructor( private val pusherSubscriber: PusherSubscriber, - private val sessionStore: SessionStore, private val userPushStoreFactory: UserPushStoreFactory, + private val pushClientSecret: PushClientSecret, private val matrixAuthenticationService: MatrixAuthenticationService, ) { - suspend fun handle(endpoint: String, pushGateway: String) { - // Register the pusher for all the sessions which are using UnifiedPush. - sessionStore.getAllSessions().toUserList().forEach { userId -> - val userDataStore = userPushStoreFactory.create(userId) - if (userDataStore.getPushProviderName() == UnifiedPushConfig.name) { - matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client -> - pusherSubscriber.registerPusher(client, endpoint, pushGateway) - } - } else { - Timber.tag(loggerTag.value).d("This session is not using UnifiedPush pusher") + suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String) { + // Register the pusher for the session with this client secret, if is it using UnifiedPush. + val userId = pushClientSecret.getUserIdFromSecret(clientSecret) ?: return Unit.also { + Timber.w("Unable to retrieve session") + } + val userDataStore = userPushStoreFactory.create(userId) + if (userDataStore.getPushProviderName() == UnifiedPushConfig.name) { + matrixAuthenticationService.restoreSession(userId).getOrNull()?.use { client -> + pusherSubscriber.registerPusher(client, endpoint, pushGateway) } + } else { + Timber.tag(loggerTag.value).d("This session is not using UnifiedPush pusher") } } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt index c12c54c0a3..854c070d7e 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import org.unifiedpush.android.connector.UnifiedPush import javax.inject.Inject @@ -29,6 +30,7 @@ class UnifiedPushProvider @Inject constructor( @ApplicationContext private val context: Context, private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, private val unRegisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, + private val pushClientSecret: PushClientSecret, ) : PushProvider { override val index = UnifiedPushConfig.index override val name = UnifiedPushConfig.name @@ -45,12 +47,14 @@ class UnifiedPushProvider @Inject constructor( } } - override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) { + override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) { + val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId) registerUnifiedPushUseCase.execute(matrixClient, distributor, clientSecret) } override suspend fun unregister(matrixClient: MatrixClient) { - unRegisterUnifiedPushUseCase.execute() + val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId) + unRegisterUnifiedPushUseCase.execute(clientSecret) } override suspend fun troubleshoot(): Result { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt index 31d7bbd63e..fe260ed24a 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt @@ -23,9 +23,6 @@ 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, @@ -35,8 +32,8 @@ class UnifiedPushStore @Inject constructor( * * @return the UnifiedPush Endpoint or null if not received */ - fun getEndpoint(): String? { - return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN, null) + fun getEndpoint(clientSecret: String): String? { + return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, null) } /** @@ -44,9 +41,9 @@ class UnifiedPushStore @Inject constructor( * * @param endpoint the endpoint to store */ - fun storeUpEndpoint(endpoint: String?) { + fun storeUpEndpoint(endpoint: String?, clientSecret: String) { defaultPrefs.edit { - putString(PREFS_ENDPOINT_OR_TOKEN, endpoint) + putString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, endpoint) } } @@ -55,8 +52,8 @@ class UnifiedPushStore @Inject constructor( * * @return the Push Gateway or null if not defined */ - fun getPushGateway(): String? { - return defaultPrefs.getString(PREFS_PUSH_GATEWAY, null) + fun getPushGateway(clientSecret: String): String? { + return defaultPrefs.getString(PREFS_PUSH_GATEWAY + clientSecret, null) } /** @@ -64,9 +61,9 @@ class UnifiedPushStore @Inject constructor( * * @param gateway the push gateway to store */ - fun storePushGateway(gateway: String?) { + fun storePushGateway(gateway: String?, clientSecret: String) { defaultPrefs.edit { - putString(PREFS_PUSH_GATEWAY, gateway) + putString(PREFS_PUSH_GATEWAY + clientSecret, gateway) } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt index 750eb95f83..e6eb778f7f 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt @@ -26,22 +26,22 @@ class UnregisterUnifiedPushUseCase @Inject constructor( @ApplicationContext private val context: Context, //private val pushDataStore: PushDataStore, private val unifiedPushStore: UnifiedPushStore, - private val unifiedPushHelper: UnifiedPushHelper, + private val unifiedPushGatewayResolver: UnifiedPushGatewayResolver, ) { - suspend fun execute(/*pushersManager: PushersManager?*/) { + suspend fun execute(clientSecret: String /*pushersManager: PushersManager?*/) { //val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME //pushDataStore.setFdroidSyncBackgroundMode(mode) try { - unifiedPushHelper.getEndpointOrToken()?.let { + unifiedPushStore.getEndpoint(clientSecret)?.let { Timber.d("Removing $it") // TODO pushersManager?.unregisterPusher(it) } } catch (e: Exception) { Timber.d(e, "Probably unregistering a non existing pusher") } - unifiedPushStore.storeUpEndpoint(null) - unifiedPushStore.storePushGateway(null) + unifiedPushStore.storeUpEndpoint(null, clientSecret) + unifiedPushStore.storePushGateway(null, clientSecret) UnifiedPush.unregisterApp(context) } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index 0736badbc2..0f065acc52 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt @@ -21,7 +21,6 @@ import android.content.Intent import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.push.providers.api.PushHandler -import io.element.android.libraries.push.providers.api.PusherSubscriber import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -33,13 +32,10 @@ private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver") class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { @Inject lateinit var pushParser: UnifiedPushParser - - // @Inject lateinit var pushDataStore: PushDataStore @Inject lateinit var pushHandler: PushHandler @Inject lateinit var guardServiceStarter: GuardServiceStarter @Inject lateinit var unifiedPushStore: UnifiedPushStore - @Inject lateinit var unifiedPushHelper: UnifiedPushHelper - @Inject lateinit var pusherSubscriber: PusherSubscriber + @Inject lateinit var unifiedPushGatewayResolver: UnifiedPushGatewayResolver @Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -71,25 +67,23 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { /** * Called when a new endpoint is to be used for sending push messages. * You should send the endpoint to your application server and sync for missing notifications. - * TODO use [instance] for multi-account */ override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint") // If the endpoint has changed // or the gateway has changed - if (unifiedPushHelper.getEndpointOrToken() != endpoint) { - unifiedPushStore.storeUpEndpoint(endpoint) + if (unifiedPushStore.getEndpoint(instance) != endpoint) { + unifiedPushStore.storeUpEndpoint(endpoint, instance) coroutineScope.launch { - unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) - unifiedPushHelper.getPushGateway()?.let { pushGateway -> - newGatewayHandler.handle(endpoint, pushGateway) + val gateway = unifiedPushGatewayResolver.getGateway(endpoint) + unifiedPushStore.storePushGateway(gateway, instance) + gateway?.let { pushGateway -> + newGatewayHandler.handle(endpoint, pushGateway, instance) } } } else { Timber.tag(loggerTag.value).i("onNewEndpoint: skipped") } - //val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED - //pushDataStore.setFdroidSyncBackgroundMode(mode) guardServiceStarter.stop() } diff --git a/libraries/pushstore/api/build.gradle.kts b/libraries/pushstore/api/build.gradle.kts index de7a852ee0..9a97bf693f 100644 --- a/libraries/pushstore/api/build.gradle.kts +++ b/libraries/pushstore/api/build.gradle.kts @@ -23,4 +23,5 @@ android { } dependencies { + implementation(projects.libraries.matrix.api) } diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt index 832180e850..52e4596ca0 100644 --- a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt @@ -16,9 +16,11 @@ package io.element.android.libraries.pushstore.api +import io.element.android.libraries.matrix.api.core.SessionId + /** * Store data related to push about a user. */ interface UserPushStoreFactory { - fun create(userId: String): UserPushStore + fun create(userId: SessionId): UserPushStore } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecret.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecret.kt similarity index 94% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecret.kt rename to libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecret.kt index 93f5f43ce4..dbdd22ce07 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecret.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecret.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.api.clientsecret import io.element.android.libraries.matrix.api.core.SessionId diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactory.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretFactory.kt similarity index 91% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactory.kt rename to libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretFactory.kt index 4ab6c775e3..128302d5c0 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactory.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretFactory.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.api.clientsecret interface PushClientSecretFactory { fun create(): String diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStore.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretStore.kt similarity index 93% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStore.kt rename to libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretStore.kt index c5f7358241..e2bd5a6084 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStore.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretStore.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.api.clientsecret import io.element.android.libraries.matrix.api.core.SessionId diff --git a/libraries/pushstore/impl/build.gradle.kts b/libraries/pushstore/impl/build.gradle.kts index 0a39730199..4625f293cb 100644 --- a/libraries/pushstore/impl/build.gradle.kts +++ b/libraries/pushstore/impl/build.gradle.kts @@ -31,8 +31,17 @@ dependencies { implementation(libs.dagger) implementation(projects.libraries.architecture) implementation(projects.libraries.core) + implementation(projects.libraries.matrix.api) implementation(projects.libraries.pushstore.api) implementation(projects.libraries.sessionStorage.api) implementation(libs.androidx.corektx) implementation(libs.androidx.datastore.preferences) + + testImplementation(libs.test.junit) + testImplementation(libs.test.mockk) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(libs.coroutines.test) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.services.appnavstate.test) } diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt index 159c1cb892..e167f5294a 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt @@ -21,6 +21,8 @@ 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.SingleIn +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.asSessionId import io.element.android.libraries.pushstore.api.UserPushStore import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.sessionstorage.api.observer.SessionListener @@ -38,8 +40,8 @@ class DefaultUserPushStoreFactory @Inject constructor( } // We can have only one class accessing a single data store, so keep a cache of them. - private val cache = mutableMapOf() - override fun create(userId: String): UserPushStore { + private val cache = mutableMapOf() + override fun create(userId: SessionId): UserPushStore { return cache.getOrPut(userId) { UserPushStoreDataStore( context = context, @@ -58,6 +60,6 @@ class DefaultUserPushStoreFactory @Inject constructor( override suspend fun onSessionDeleted(userId: String) { // Delete the store - create(userId).reset() + create(userId.asSessionId()).reset() } } diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt index 0e4e668eda..c7a320f085 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt @@ -22,6 +22,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushstore.api.UserPushStore import kotlinx.coroutines.flow.first @@ -30,7 +31,7 @@ import kotlinx.coroutines.flow.first */ class UserPushStoreDataStore( private val context: Context, - userId: String, + userId: SessionId, ) : UserPushStore { private val Context.dataStore: DataStore by preferencesDataStore(name = "push_store_$userId") private val pushProviderName = stringPreferencesKey("pushProviderName") diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretFactoryImpl.kt similarity index 86% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt rename to libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretFactoryImpl.kt index 1d7a1e6247..4e6e718a60 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretFactoryImpl.kt @@ -14,10 +14,11 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.impl.clientsecret import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory import java.util.UUID import javax.inject.Inject diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImpl.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImpl.kt similarity index 84% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImpl.kt rename to libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImpl.kt index b57b24d25e..ca0ed14e33 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImpl.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImpl.kt @@ -14,11 +14,14 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.impl.clientsecret import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore import javax.inject.Inject @ContributesBinding(AppScope::class) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretStoreDataStore.kt similarity index 94% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStoreDataStore.kt rename to libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretStoreDataStore.kt index 055de6fc47..2431120c9e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretStoreDataStore.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.impl.clientsecret import android.content.Context import androidx.datastore.core.DataStore @@ -27,6 +27,7 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.asSessionId +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore import kotlinx.coroutines.flow.first import javax.inject.Inject diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/FakePushClientSecretFactory.kt b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/FakePushClientSecretFactory.kt similarity index 85% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/FakePushClientSecretFactory.kt rename to libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/FakePushClientSecretFactory.kt index 25823a57e8..b1cb93e49c 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/FakePushClientSecretFactory.kt +++ b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/FakePushClientSecretFactory.kt @@ -14,7 +14,9 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.impl.clientsecret + +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory private const val A_SECRET_PREFIX = "A_SECRET_" diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/InMemoryPushClientSecretStore.kt b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/InMemoryPushClientSecretStore.kt similarity index 89% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/InMemoryPushClientSecretStore.kt rename to libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/InMemoryPushClientSecretStore.kt index a2d2d9c83c..8c9b577967 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/InMemoryPushClientSecretStore.kt +++ b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/InMemoryPushClientSecretStore.kt @@ -14,9 +14,10 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.impl.clientsecret import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore class InMemoryPushClientSecretStore : PushClientSecretStore { private val secrets = mutableMapOf() diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImplTest.kt b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImplTest.kt similarity index 97% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImplTest.kt rename to libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImplTest.kt index a9d740bf31..d7f8e2e337 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImplTest.kt +++ b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImplTest.kt @@ -16,7 +16,7 @@ @file:OptIn(ExperimentalCoroutinesApi::class) -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.impl.clientsecret import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.SessionId From f4a283567ea4c403d8f4665235e2ce359f31f938 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Apr 2023 16:48:51 +0200 Subject: [PATCH 083/119] Cleanup store. --- .../libraries/push/api/store/PushDataStore.kt | 18 ---- .../NotificationDrawerManager.kt | 11 +-- .../notifications/NotificationEventQueue.kt | 1 + .../push/impl/push/DefaultPushHandler.kt | 23 +++-- .../push/impl/store/DefaultPushDataStore.kt | 96 ------------------- .../libraries/pushstore/api/UserPushStore.kt | 11 ++- .../pushstore/impl/UserPushStoreDataStore.kt | 17 ++++ 7 files changed, 40 insertions(+), 137 deletions(-) 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 index d2a6bda0b0..f478034063 100644 --- 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 @@ -16,26 +16,8 @@ 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 - - // TODO Move all those settings to the per user store... - 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/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 8d3bfda4c3..cf0307fbd9 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 @@ -70,7 +70,8 @@ class NotificationDrawerManager @Inject constructor( private var currentAppNavigationState: AppNavigationState? = null private val firstThrottler = FirstThrottler(200) - private var useCompleteNotificationFormat = pushDataStore.useCompleteNotificationFormat() + // TODO EAx add a setting per user for this + private var useCompleteNotificationFormat = true init { handlerThread.start() @@ -111,12 +112,6 @@ class NotificationDrawerManager @Inject constructor( } private 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 { @@ -185,7 +180,7 @@ class NotificationDrawerManager @Inject constructor( // TODO EAx Must be per account fun notificationStyleChanged() { updateEvents { - val newSettings = pushDataStore.useCompleteNotificationFormat() + val newSettings = true // pushDataStore.useCompleteNotificationFormat() if (newSettings != useCompleteNotificationFormat) { // Settings has changed, remove all current notifications notificationRenderer.cancelAllNotifications() 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 index 60fb1baa05..862b4784ac 100644 --- 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 @@ -34,6 +34,7 @@ data class NotificationEventQueue constructor( * Acts as a notification debouncer to stop already dismissed push notifications from * displaying again when the /sync response is delayed. */ + // TODO Should be per session, so the key must be Pair. private val seenEventIds: CircularCache ) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index 7eef95fc67..09afe0a861 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -22,15 +22,12 @@ import android.os.Handler import android.os.Looper import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.squareup.anvil.annotations.ContributesBinding -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.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.push.api.store.PushDataStore import io.element.android.libraries.push.impl.PushersManager -import io.element.android.libraries.pushstore.api.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 @@ -38,6 +35,8 @@ import io.element.android.libraries.push.impl.notifications.NotificationDrawerMa import io.element.android.libraries.push.impl.store.DefaultPushDataStore import io.element.android.libraries.push.providers.api.PushData import io.element.android.libraries.push.providers.api.PushHandler +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -51,17 +50,16 @@ private val loggerTag = LoggerTag("PushHandler", pushLoggerTag) class DefaultPushHandler @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val notifiableEventResolver: NotifiableEventResolver, - private val pushDataStore: PushDataStore, private val defaultPushDataStore: DefaultPushDataStore, + private val userPushStoreFactory: UserPushStoreFactory, private val pushClientSecret: PushClientSecret, private val actionIds: NotificationActionIds, @ApplicationContext private val context: Context, private val buildMeta: BuildMeta, private val matrixAuthenticationService: MatrixAuthenticationService, -): PushHandler { +) : PushHandler { private val coroutineScope = CoroutineScope(SupervisorJob()) - private val wifiDetector: WifiDetector = WifiDetector(context) // UI handler private val mUIHandler by lazy { @@ -89,12 +87,6 @@ class DefaultPushHandler @Inject constructor( return } - // TODO EAx Should be per user - if (!pushDataStore.areNotificationEnabledForDevice()) { - Timber.tag(loggerTag.value).i("Notification are disabled for this device") - return - } - mUIHandler.post { coroutineScope.launch(Dispatchers.IO) { handleInternal(pushData) } } @@ -139,6 +131,13 @@ class DefaultPushHandler @Inject constructor( return } + val userPushStore = userPushStoreFactory.create(userId) + if (!userPushStore.areNotificationEnabledForDevice()) { + // TODO We need to check if this is an incoming call + Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.") + return + } + notificationDrawerManager.onNotifiableEventReceived(notificationData) } catch (e: Exception) { Timber.tag(loggerTag.value).e(e, "## handleInternal() failed") 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 ffbd575aa4..22faa91453 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 @@ -17,20 +17,15 @@ 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 @@ -42,7 +37,6 @@ private val Context.dataStore: DataStore by preferencesDataStore(na @ContributesBinding(AppScope::class) class DefaultPushDataStore @Inject constructor( @ApplicationContext private val context: Context, - @DefaultPreferences private val defaultPrefs: SharedPreferences, ) : PushDataStore { private val pushCounter = intPreferencesKey("push_counter") @@ -56,94 +50,4 @@ class DefaultPushDataStore @Inject constructor( 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/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt index 6817199e13..28577ba3f8 100644 --- a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt @@ -21,12 +21,17 @@ package io.element.android.libraries.pushstore.api */ interface UserPushStore { suspend fun getPushProviderName(): String? - suspend fun setPushProviderName(value: String) - suspend fun getCurrentRegisteredPushKey(): String? - suspend fun setCurrentRegisteredPushKey(value: String) + suspend fun areNotificationEnabledForDevice(): Boolean + suspend fun setNotificationEnabledForDevice(enabled: Boolean) + + /** + * Return true if Pin code is disabled, or if user set the settings to see full notification content. + */ + fun useCompleteNotificationFormat(): Boolean + suspend fun reset() } diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt index c7a320f085..56867a6584 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt @@ -19,9 +19,11 @@ package io.element.android.libraries.pushstore.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.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushstore.api.UserPushStore import kotlinx.coroutines.flow.first @@ -36,6 +38,7 @@ class UserPushStoreDataStore( private val Context.dataStore: DataStore by preferencesDataStore(name = "push_store_$userId") private val pushProviderName = stringPreferencesKey("pushProviderName") private val currentPushKey = stringPreferencesKey("currentPushKey") + private val notificationEnabled = booleanPreferencesKey("notificationEnabled") override suspend fun getPushProviderName(): String? { return context.dataStore.data.first()[pushProviderName] @@ -57,6 +60,20 @@ class UserPushStoreDataStore( } } + override suspend fun areNotificationEnabledForDevice(): Boolean { + return context.dataStore.data.first()[notificationEnabled].orTrue() + } + + override suspend fun setNotificationEnabledForDevice(enabled: Boolean) { + context.dataStore.edit { + it[notificationEnabled] = enabled + } + } + + override fun useCompleteNotificationFormat(): Boolean { + return true + } + override suspend fun reset() { context.dataStore.edit { it.clear() From 7f22c6b211b3a3dd1a7f47cd6d30318a66a582e2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Apr 2023 17:02:14 +0200 Subject: [PATCH 084/119] Use Firebase by default and cleanup --- .../appnav/loggedin/LoggedInPresenter.kt | 4 +- .../push/api/model/BackgroundSyncMode.kt | 48 ------------------- .../push/providers/api/PushProvider.kt | 8 ++-- .../firebase/FirebasePushProvider.kt | 2 - 4 files changed, 6 insertions(+), 56 deletions(-) delete mode 100644 libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/model/BackgroundSyncMode.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 a798f95bae..50bc09d0a0 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 @@ -47,8 +47,8 @@ class LoggedInPresenter @Inject constructor( LaunchedEffect(Unit) { // Ensure pusher is registered // TODO Manually select push provider for now - val pushProvider = pushService.getAvailablePushProviders().find { it.name == "UnifiedPush" } ?: return@LaunchedEffect - val distributor = pushProvider.getDistributors().first() + val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect + val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect pushService.registerWith(matrixClient, pushProvider, distributor) } 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 deleted file mode 100644 index 3fb4841aba..0000000000 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/model/BackgroundSyncMode.kt +++ /dev/null @@ -1,48 +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.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/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt index ebcdd962d6..7e16121ed4 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt @@ -23,7 +23,7 @@ import io.element.android.libraries.matrix.api.MatrixClient */ interface PushProvider { /** - * Allow to sort provider, from lower index to higher index + * Allow to sort providers, from lower index to higher index. */ val index: Int @@ -35,17 +35,17 @@ interface PushProvider { fun getDistributors(): List /** - * Register the pusher to the homeserver + * Register the pusher to the homeserver. */ suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) /** - * Unregister the pusher + * Unregister the pusher. */ suspend fun unregister(matrixClient: MatrixClient) /** - * Attempt to troubleshoot the push provider + * Attempt to troubleshoot the push provider. */ suspend fun troubleshoot(): Result } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt index cfddff0bdf..15530033d5 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt @@ -21,7 +21,6 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider import io.element.android.libraries.push.providers.api.PusherSubscriber -import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import timber.log.Timber import javax.inject.Inject @@ -31,7 +30,6 @@ class FirebasePushProvider @Inject constructor( private val firebaseStore: FirebaseStore, private val firebaseTroubleshooter: FirebaseTroubleshooter, private val pusherSubscriber: PusherSubscriber, - private val pushClientSecret: PushClientSecret, ) : PushProvider { override val index = FirebaseConfig.index override val name = FirebaseConfig.name From ab1b1ab1cb4f856196f7f50b9d8ee9f7f7cb2ef1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Apr 2023 17:09:12 +0200 Subject: [PATCH 085/119] Fix test --- .../push/providers/firebase/FirebasePushParserTest.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt index a6525657c8..466a2bfedf 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt @@ -20,6 +20,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.push.providers.api.PushData +import org.junit.Assert.assertThrows import org.junit.Test class FirebasePushParserTest { @@ -51,26 +52,26 @@ class FirebasePushParserTest { fun `test empty roomId`() { val pushParser = FirebasePushParser() assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", null))).isNull() - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", ""))).isNull() + assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "")) } } @Test fun `test invalid roomId`() { val pushParser = FirebasePushParser() - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain"))).isNull() + assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain")) } } @Test fun `test empty eventId`() { val pushParser = FirebasePushParser() assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", null))).isNull() - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", ""))).isNull() + assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "")) } } @Test fun `test invalid eventId`() { val pushParser = FirebasePushParser() - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId"))).isNull() + assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId")) } } companion object { From f4e4be7479aca85b76cf292c297d938636326e43 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 Apr 2023 09:14:55 +0200 Subject: [PATCH 086/119] Improve asXId and make tests pass in release and debug mode. --- .../libraries/matrix/api/core/EventId.kt | 8 +++-- .../libraries/matrix/api/core/RoomId.kt | 8 +++-- .../libraries/matrix/api/core/SessionId.kt | 8 +++-- .../libraries/matrix/api/core/SpaceId.kt | 8 +++-- .../libraries/matrix/api/core/ThreadId.kt | 10 ++++-- .../libraries/matrix/api/core/UserId.kt | 8 +++-- .../pushproviders/firebase/build.gradle.kts | 1 + .../firebase/FirebaseNewTokenHandler.kt | 2 +- .../firebase/FirebasePushParserTest.kt | 10 +++--- .../unifiedpush/build.gradle.kts | 1 + .../unifiedpush/UnifiedPushParserTest.kt | 10 +++--- .../impl/DefaultUserPushStoreFactory.kt | 2 +- settings.gradle.kts | 1 + tests/testutils/build.gradle.kts | 36 +++++++++++++++++++ .../android/tests/testutils/NullOrThrow.kt | 33 +++++++++++++++++ 15 files changed, 121 insertions(+), 25 deletions(-) create mode 100644 tests/testutils/build.gradle.kts create mode 100644 tests/testutils/src/main/kotlin/io/element/android/tests/testutils/NullOrThrow.kt diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt index ffd5bb8ea2..b24d886ee8 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt @@ -22,8 +22,12 @@ import java.io.Serializable @JvmInline value class EventId(val value: String) : Serializable -fun String.asEventId() = EventId(this).also { - if (BuildConfig.DEBUG && !MatrixPatterns.isEventId(this)) { +fun String.asEventId() = if (MatrixPatterns.isEventId(this)) { + EventId(this) +} else { + if (BuildConfig.DEBUG) { error("`$this` is not a valid event Id") + } else { + null } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt index f711723c3f..f71f4ba4f9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt @@ -22,8 +22,12 @@ import java.io.Serializable @JvmInline value class RoomId(val value: String) : Serializable -fun String.asRoomId() = RoomId(this).also { - if (BuildConfig.DEBUG && !MatrixPatterns.isRoomId(this)) { +fun String.asRoomId() = if (MatrixPatterns.isRoomId(this)) { + RoomId(this) +} else { + if (BuildConfig.DEBUG) { error("`$this` is not a valid room Id") + } else { + null } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt index 8591876b29..0f0edf2299 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt @@ -20,8 +20,12 @@ import io.element.android.libraries.matrix.api.BuildConfig typealias SessionId = UserId -fun String.asSessionId() = SessionId(this).also { - if (BuildConfig.DEBUG && !MatrixPatterns.isSessionId(this)) { +fun String.asSessionId() = if (MatrixPatterns.isSessionId(this)) { + SessionId(this) +} else { + if (BuildConfig.DEBUG) { error("`$this` is not a valid session Id") + } else { + null } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt index 1b8b33426b..d4f2e43be6 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt @@ -27,8 +27,12 @@ value class SpaceId(val value: String) : Serializable */ val MAIN_SPACE = SpaceId("!mainSpace") -fun String.asSpaceId() = SpaceId(this).also { - if (BuildConfig.DEBUG && !MatrixPatterns.isSpaceId(this)) { +fun String.asSpaceId() = if (MatrixPatterns.isSpaceId(this)) { + SpaceId(this) +} else { + if (BuildConfig.DEBUG) { error("`$this` is not a valid space Id") + } else { + null } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt index f95c33bad3..f57cb8fa23 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt @@ -22,8 +22,12 @@ import java.io.Serializable @JvmInline value class ThreadId(val value: String) : Serializable -fun String.asThreadId() = ThreadId(this).also { - if (BuildConfig.DEBUG && !MatrixPatterns.isThreadId(this)) { - error("`$this` is not a valid Thread Id") +fun String.asThreadId() = if (MatrixPatterns.isThreadId(this)) { + ThreadId(this) +} else { + if (BuildConfig.DEBUG) { + error("`$this` is not a valid thread Id") + } else { + null } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt index 91f9c6f11c..ba7028c926 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt @@ -22,8 +22,12 @@ import java.io.Serializable @JvmInline value class UserId(val value: String) : Serializable -fun String.asUserId() = UserId(this).also { - if (BuildConfig.DEBUG && !MatrixPatterns.isUserId(this)) { +fun String.asUserId() = if (MatrixPatterns.isUserId(this)) { + UserId(this) +} else { + if (BuildConfig.DEBUG) { error("`$this` is not a valid user Id") + } else { + null } } diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index eabc70305b..a20398319b 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -45,4 +45,5 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt index f26fcfae25..58464b5af0 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt @@ -42,7 +42,7 @@ class FirebaseNewTokenHandler @Inject constructor( firebaseStore.storeFcmToken(firebaseToken) // Register the pusher for all the sessions sessionStore.getAllSessions().toUserList() - .map { it.asSessionId() } + .mapNotNull { it.asSessionId() } .forEach { userId -> val userDataStore = userPushStoreFactory.create(userId) if (userDataStore.getPushProviderName() == FirebaseConfig.name) { diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt index 466a2bfedf..562aecc790 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt @@ -20,7 +20,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.push.providers.api.PushData -import org.junit.Assert.assertThrows +import io.element.android.tests.testutils.assertNullOrThrow import org.junit.Test class FirebasePushParserTest { @@ -52,26 +52,26 @@ class FirebasePushParserTest { fun `test empty roomId`() { val pushParser = FirebasePushParser() assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", null))).isNull() - assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "")) } + assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "")) } } @Test fun `test invalid roomId`() { val pushParser = FirebasePushParser() - assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain")) } + assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain")) } } @Test fun `test empty eventId`() { val pushParser = FirebasePushParser() assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", null))).isNull() - assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "")) } + assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "")) } } @Test fun `test invalid eventId`() { val pushParser = FirebasePushParser() - assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId")) } + assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId")) } } companion object { diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts index 7dcd773c25..3546bb16e1 100644 --- a/libraries/pushproviders/unifiedpush/build.gradle.kts +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -53,4 +53,5 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) } diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt index b18be39e07..19231505cc 100644 --- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt @@ -20,7 +20,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.push.providers.api.PushData -import org.junit.Assert.assertThrows +import io.element.android.tests.testutils.assertNullOrThrow import org.junit.Test class UnifiedPushParserTest { @@ -52,7 +52,7 @@ class UnifiedPushParserTest { @Test fun `test empty roomId`() { val pushParser = UnifiedPushParser() - assertThrows(IllegalStateException::class.java) { + assertNullOrThrow { pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray(), aClientSecret) } } @@ -60,7 +60,7 @@ class UnifiedPushParserTest { @Test fun `test invalid roomId`() { val pushParser = UnifiedPushParser() - assertThrows(IllegalStateException::class.java) { + assertNullOrThrow { pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"), aClientSecret) } } @@ -68,7 +68,7 @@ class UnifiedPushParserTest { @Test fun `test empty eventId`() { val pushParser = UnifiedPushParser() - assertThrows(IllegalStateException::class.java) { + assertNullOrThrow { pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""), aClientSecret) } } @@ -76,7 +76,7 @@ class UnifiedPushParserTest { @Test fun `test invalid eventId`() { val pushParser = UnifiedPushParser() - assertThrows(IllegalStateException::class.java) { + assertNullOrThrow { pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"), aClientSecret) } } diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt index e167f5294a..ed32dba472 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt @@ -60,6 +60,6 @@ class DefaultUserPushStoreFactory @Inject constructor( override suspend fun onSessionDeleted(userId: String) { // Delete the store - create(userId.asSessionId()).reset() + userId.asSessionId()?.let { create(it).reset() } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 7429f80b43..1173288adb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -49,6 +49,7 @@ rootProject.name = "ElementX" include(":app") include(":appnav") include(":tests:uitests") +include(":tests:testutils") include(":anvilannotations") include(":anvilcodegen") diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts new file mode 100644 index 0000000000..0c28da1f06 --- /dev/null +++ b/tests/testutils/build.gradle.kts @@ -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. + */ + +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("io.element.android-library") + alias(libs.plugins.ksp) +} + +android { + namespace = "io.element.android.tests.testutils" +} + +dependencies { + implementation(libs.test.junit) + implementation(libs.test.mockk) + implementation(libs.test.truth) + implementation(libs.test.turbine) + implementation(libs.coroutines.test) + implementation(projects.libraries.matrix.test) + implementation(projects.services.appnavstate.test) +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/NullOrThrow.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/NullOrThrow.kt new file mode 100644 index 0000000000..adfd58da5f --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/NullOrThrow.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.tests.testutils + +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows + +/** + * Assert that the lambda throws on debug and returns null on release. + */ +fun assertNullOrThrow(lambda: () -> Any?) { + if (BuildConfig.DEBUG) { + assertThrows(IllegalStateException::class.java) { + lambda() + } + } else { + assertThat(lambda()).isNull() + } +} From 245c46c8b860db3a11eb11a4b1a7c7b274b057ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 Apr 2023 10:06:11 +0200 Subject: [PATCH 087/119] Cleanup --- libraries/push/impl/build.gradle.kts | 5 ++--- .../libraries/push/impl/DefaultPushService.kt | 2 +- .../libraries/push/impl/PushersManager.kt | 21 ++----------------- .../push/providers/api/PushProvider.kt | 2 +- .../pushproviders/firebase/build.gradle.kts | 1 - .../providers/unifiedpush/UnifiedPushStore.kt | 4 ++++ 6 files changed, 10 insertions(+), 25 deletions(-) diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 44fc21fb2b..81ba07fc63 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -55,9 +55,8 @@ dependencies { exclude(group = "com.android.support", module = "support-annotations") } - // TODO Remove - implementation(platform(libs.google.firebase.bom)) - implementation("com.google.firebase:firebase-messaging-ktx") + // TODO Temporary use the deprecated LocalBroadcastManager, to be changed later. + implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0") testImplementation(libs.test.junit) testImplementation(libs.test.mockk) 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 99af1317f9..28a58d1058 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 @@ -43,7 +43,7 @@ class DefaultPushService @Inject constructor( } /** - * Get current push provider, compare with provided one, then unregister and register if different, and store change + * Get current push provider, compare with provided one, then unregister and register if different, and store change. */ override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) { val userPushStore = userPushStoreFactory.create(matrixClient.sessionId) 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 33b8ac3507..04d2875328 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 @@ -20,17 +20,15 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.AppScope 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.EventId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData -import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret 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.pushgateway.PushGatewayNotifyRequest import io.element.android.libraries.push.providers.api.PusherSubscriber import io.element.android.libraries.pushstore.api.UserPushStoreFactory -import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.services.toolbox.api.appname.AppNameProvider import timber.log.Timber import javax.inject.Inject @@ -41,16 +39,13 @@ private val loggerTag = LoggerTag("PushersManager", pushLoggerTag) @ContributesBinding(AppScope::class) class PushersManager @Inject constructor( - // private val unifiedPushHelper: UnifiedPushHelper, // private val localeProvider: LocaleProvider, private val appNameProvider: AppNameProvider, // private val getDeviceInfoUseCase: GetDeviceInfoUseCase, private val pushGatewayNotifyRequest: PushGatewayNotifyRequest, private val pushClientSecret: PushClientSecret, - private val sessionStore: SessionStore, - private val matrixAuthenticationService: MatrixAuthenticationService, private val userPushStoreFactory: UserPushStoreFactory, -): PusherSubscriber { +) : PusherSubscriber { // TODO Move this to the PushProvider API suspend fun testPush() { pushGatewayNotifyRequest.execute( @@ -63,18 +58,6 @@ class PushersManager @Inject constructor( ) } - suspend fun enqueueRegisterPusherWithFcmKey(pushKey: String) { - // return onNewFirebaseToken(pushKey, PushConfig.pusher_http_url) - TODO() - } - - suspend fun onNewUnifiedPushEndpoint( - pushKey: String, - gateway: String - ) { - TODO() - } - /** * Register a pusher to the server if not done yet. */ diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt index 7e16121ed4..4ad0179403 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt @@ -19,7 +19,7 @@ package io.element.android.libraries.push.providers.api import io.element.android.libraries.matrix.api.MatrixClient /** - * This is the main API for this module + * This is the main API for this module. */ interface PushProvider { /** diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index a20398319b..b9b4f76c12 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -17,7 +17,6 @@ plugins { id("io.element.android-library") alias(libs.plugins.anvil) - // kotlin("plugin.serialization") version "1.8.10" } android { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt index fe260ed24a..3883c3348c 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt @@ -30,6 +30,7 @@ class UnifiedPushStore @Inject constructor( /** * Retrieves the UnifiedPush Endpoint. * + * @param clientSecret the client secret, to identify the session * @return the UnifiedPush Endpoint or null if not received */ fun getEndpoint(clientSecret: String): String? { @@ -40,6 +41,7 @@ class UnifiedPushStore @Inject constructor( * Store UnifiedPush Endpoint to the SharedPrefs. * * @param endpoint the endpoint to store + * @param clientSecret the client secret, to identify the session */ fun storeUpEndpoint(endpoint: String?, clientSecret: String) { defaultPrefs.edit { @@ -50,6 +52,7 @@ class UnifiedPushStore @Inject constructor( /** * Retrieves the Push Gateway. * + * @param clientSecret the client secret, to identify the session * @return the Push Gateway or null if not defined */ fun getPushGateway(clientSecret: String): String? { @@ -60,6 +63,7 @@ class UnifiedPushStore @Inject constructor( * Store Push Gateway to the SharedPrefs. * * @param gateway the push gateway to store + * @param clientSecret the client secret, to identify the session */ fun storePushGateway(gateway: String?, clientSecret: String) { defaultPrefs.edit { From 32b09f2e61385034d46e9b1608b53cf40ec5323a 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 088/119] 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 f81ce0e835..2f39ffda8b 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" From e9fa854143b127aa4e97108d2cc9be54ef601054 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 Apr 2023 11:15:47 +0200 Subject: [PATCH 089/119] Fix issue with lint. --- libraries/pushproviders/firebase/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index b9b4f76c12..17f2071624 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -38,7 +38,7 @@ dependencies { implementation(projects.libraries.pushstore.api) implementation(projects.libraries.pushproviders.api) - implementation(platform(libs.google.firebase.bom)) + api(platform(libs.google.firebase.bom)) api("com.google.firebase:firebase-messaging-ktx") testImplementation(libs.test.junit) From 1f7b89721692ac12bdcceaac390f317b785e6cf1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 Apr 2023 12:03:11 +0200 Subject: [PATCH 090/119] Add firebase resource file generated by the firebase plugin, to be able to remove the plugin. --- app/build.gradle.kts | 4 +- app/src/debug/google-services.json | 49 ------------------- app/src/nightly/google-services.json | 40 --------------- app/src/release/google-services.json | 40 --------------- libraries/pushproviders/firebase/README.md | 7 +++ .../src/debug/res/values/firebase.xml | 4 ++ .../firebase/src/main/res/values/firebase.xml | 10 ++++ .../src/nightly/res/values/firebase.xml | 4 ++ .../src/release/res/values/firebase.xml | 4 ++ 9 files changed, 31 insertions(+), 131 deletions(-) delete mode 100644 app/src/debug/google-services.json delete mode 100644 app/src/nightly/google-services.json delete mode 100644 app/src/release/google-services.json create mode 100644 libraries/pushproviders/firebase/README.md create mode 100644 libraries/pushproviders/firebase/src/debug/res/values/firebase.xml create mode 100644 libraries/pushproviders/firebase/src/main/res/values/firebase.xml create mode 100644 libraries/pushproviders/firebase/src/nightly/res/values/firebase.xml create mode 100644 libraries/pushproviders/firebase/src/release/res/values/firebase.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 74ebfa89d4..772609f482 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,8 +33,8 @@ plugins { id("com.google.firebase.appdistribution") version "4.0.0" id("org.jetbrains.kotlinx.knit") version "0.4.0" id("kotlin-parcelize") - // TODO Move the plugin to the firebase module? - id("com.google.gms.google-services") + // To be able to update the firebase.xml files, uncomment and build the project + // id("com.google.gms.google-services") } android { diff --git a/app/src/debug/google-services.json b/app/src/debug/google-services.json deleted file mode 100644 index d9aa72f7ba..0000000000 --- a/app/src/debug/google-services.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "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 deleted file mode 100644 index 31b022b3f2..0000000000 --- a/app/src/nightly/google-services.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "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:e17435e0beb0303000427c", - "android_client_info": { - "package_name": "io.element.android.x.nightly" - } - }, - "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" -} diff --git a/app/src/release/google-services.json b/app/src/release/google-services.json deleted file mode 100644 index 16fd1e855c..0000000000 --- a/app/src/release/google-services.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "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" -} diff --git a/libraries/pushproviders/firebase/README.md b/libraries/pushproviders/firebase/README.md new file mode 100644 index 0000000000..204ac6dd19 --- /dev/null +++ b/libraries/pushproviders/firebase/README.md @@ -0,0 +1,7 @@ +# Firebase + +## Configuration + +In order to make this module only know about Firebase, the plugin `com.google.gms.google-services` has been disabled from the `app` module. + +To be able to change the values in the file `firebase.xml` from this module, you should enable the plugin `com.google.gms.google-services` again, copy the file `google-services.json` to the folder `/app/src/main`, build the project, and check the generated file `app/build/generated/res/google-services//values/values.xml` to import the generated values into the `firebase.xml` files. diff --git a/libraries/pushproviders/firebase/src/debug/res/values/firebase.xml b/libraries/pushproviders/firebase/src/debug/res/values/firebase.xml new file mode 100644 index 0000000000..540f0e9bbe --- /dev/null +++ b/libraries/pushproviders/firebase/src/debug/res/values/firebase.xml @@ -0,0 +1,4 @@ + + + 1:912726360885:android:def0a4e454042e9b00427c + diff --git a/libraries/pushproviders/firebase/src/main/res/values/firebase.xml b/libraries/pushproviders/firebase/src/main/res/values/firebase.xml new file mode 100644 index 0000000000..163717db91 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values/firebase.xml @@ -0,0 +1,10 @@ + + + 912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com + https://vector-alpha.firebaseio.com + 912726360885 + AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c + AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c + vector-alpha.appspot.com + vector-alpha + diff --git a/libraries/pushproviders/firebase/src/nightly/res/values/firebase.xml b/libraries/pushproviders/firebase/src/nightly/res/values/firebase.xml new file mode 100644 index 0000000000..f793ba93f9 --- /dev/null +++ b/libraries/pushproviders/firebase/src/nightly/res/values/firebase.xml @@ -0,0 +1,4 @@ + + + 1:912726360885:android:e17435e0beb0303000427c + diff --git a/libraries/pushproviders/firebase/src/release/res/values/firebase.xml b/libraries/pushproviders/firebase/src/release/res/values/firebase.xml new file mode 100644 index 0000000000..d563b43d05 --- /dev/null +++ b/libraries/pushproviders/firebase/src/release/res/values/firebase.xml @@ -0,0 +1,4 @@ + + + 1:912726360885:android:d097de99a4c23d2700427c + From 798629faf73a2b888c5403c1bfb7b72015df83bc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 13 Apr 2023 13:00:16 +0200 Subject: [PATCH 091/119] Do check only on Debug --- .../android/libraries/matrix/api/core/EventId.kt | 10 +++------- .../android/libraries/matrix/api/core/RoomId.kt | 10 +++------- .../android/libraries/matrix/api/core/SessionId.kt | 10 +++------- .../android/libraries/matrix/api/core/SpaceId.kt | 10 +++------- .../android/libraries/matrix/api/core/ThreadId.kt | 10 +++------- .../android/libraries/matrix/api/core/UserId.kt | 10 +++------- 6 files changed, 18 insertions(+), 42 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt index b24d886ee8..a6e6d7edb3 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt @@ -22,12 +22,8 @@ import java.io.Serializable @JvmInline value class EventId(val value: String) : Serializable -fun String.asEventId() = if (MatrixPatterns.isEventId(this)) { - EventId(this) +fun String.asEventId() = if (BuildConfig.DEBUG && !MatrixPatterns.isEventId(this)) { + error("`$this` is not a valid event Id") } else { - if (BuildConfig.DEBUG) { - error("`$this` is not a valid event Id") - } else { - null - } + EventId(this) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt index f71f4ba4f9..e31b8063df 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt @@ -22,12 +22,8 @@ import java.io.Serializable @JvmInline value class RoomId(val value: String) : Serializable -fun String.asRoomId() = if (MatrixPatterns.isRoomId(this)) { - RoomId(this) +fun String.asRoomId() = if (BuildConfig.DEBUG && !MatrixPatterns.isRoomId(this)) { + error("`$this` is not a valid room Id") } else { - if (BuildConfig.DEBUG) { - error("`$this` is not a valid room Id") - } else { - null - } + RoomId(this) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt index 0f0edf2299..f6d45dc6df 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt @@ -20,12 +20,8 @@ import io.element.android.libraries.matrix.api.BuildConfig typealias SessionId = UserId -fun String.asSessionId() = if (MatrixPatterns.isSessionId(this)) { - SessionId(this) +fun String.asSessionId() = if (BuildConfig.DEBUG && !MatrixPatterns.isSessionId(this)) { + error("`$this` is not a valid session Id") } else { - if (BuildConfig.DEBUG) { - error("`$this` is not a valid session Id") - } else { - null - } + SessionId(this) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt index d4f2e43be6..342a13d693 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt @@ -27,12 +27,8 @@ value class SpaceId(val value: String) : Serializable */ val MAIN_SPACE = SpaceId("!mainSpace") -fun String.asSpaceId() = if (MatrixPatterns.isSpaceId(this)) { - SpaceId(this) +fun String.asSpaceId() = if (BuildConfig.DEBUG && !MatrixPatterns.isSpaceId(this)) { + error("`$this` is not a valid space Id") } else { - if (BuildConfig.DEBUG) { - error("`$this` is not a valid space Id") - } else { - null - } + SpaceId(this) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt index f57cb8fa23..7599cd8a6a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt @@ -22,12 +22,8 @@ import java.io.Serializable @JvmInline value class ThreadId(val value: String) : Serializable -fun String.asThreadId() = if (MatrixPatterns.isThreadId(this)) { - ThreadId(this) +fun String.asThreadId() = if (BuildConfig.DEBUG && !MatrixPatterns.isThreadId(this)) { + error("`$this` is not a valid thread Id") } else { - if (BuildConfig.DEBUG) { - error("`$this` is not a valid thread Id") - } else { - null - } + ThreadId(this) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt index ba7028c926..46adcdd59c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt @@ -22,12 +22,8 @@ import java.io.Serializable @JvmInline value class UserId(val value: String) : Serializable -fun String.asUserId() = if (MatrixPatterns.isUserId(this)) { - UserId(this) +fun String.asUserId() = if (BuildConfig.DEBUG && !MatrixPatterns.isUserId(this)) { + error("`$this` is not a valid user Id") } else { - if (BuildConfig.DEBUG) { - error("`$this` is not a valid user Id") - } else { - null - } + UserId(this) } From 0874c076d6c001cd7f1142cdca52b9474110fffc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 13 Apr 2023 15:04:51 +0200 Subject: [PATCH 092/119] Deeplink: handle notification click to open a room. --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 8 ++++ .../io/element/android/x/MainActivity.kt | 22 ++++++++- .../kotlin/io/element/android/x/MainNode.kt | 20 +++++++- .../android/x/intent/IntentProviderImpl.kt | 20 ++++---- appnav/build.gradle.kts | 1 + .../android/appnav/LoggedInFlowNode.kt | 17 +++++-- .../io/element/android/appnav/RoomFlowNode.kt | 1 - .../io/element/android/appnav/RootFlowNode.kt | 30 ++++++++++++ libraries/deeplink/build.gradle.kts | 40 ++++++++++++++++ .../libraries/deeplink/DeepLinkCreator.kt | 39 +++++++++++++++ .../libraries/deeplink/DeeplinkData.kt | 27 +++++++++++ .../libraries/deeplink/DeeplinkParser.kt | 47 +++++++++++++++++++ .../push/impl/intent/IntentProvider.kt | 4 +- .../notifications/NotificationActionIds.kt | 1 - .../impl/notifications/NotificationUtils.kt | 19 ++------ tools/adb/deeplink.sh | 28 +++++++++++ 17 files changed, 292 insertions(+), 33 deletions(-) create mode 100644 libraries/deeplink/build.gradle.kts create mode 100644 libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt create mode 100644 libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt create mode 100644 libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt create mode 100755 tools/adb/deeplink.sh diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 772609f482..7a90fc62b8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -206,6 +206,7 @@ dependencies { allLibrariesImpl() allServicesImpl() allFeaturesImpl(rootDir) + implementation(projects.libraries.deeplink) implementation(projects.tests.uitests) implementation(projects.anvilannotations) implementation(projects.appnav) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 828788ed80..342e05532c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,14 @@ + + { + override fun init(node: MainNode) { + mainNode = node + mainNode.handleIntent(intent) + } + } + ) + ) } } } @@ -63,6 +79,8 @@ class MainActivity : NodeComponentActivity() { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) Timber.w("onNewIntent") + intent ?: return + mainNode.handleIntent(intent) } override fun onSaveInstanceState(outState: Bundle) { diff --git a/app/src/main/kotlin/io/element/android/x/MainNode.kt b/app/src/main/kotlin/io/element/android/x/MainNode.kt index 6b7dee92b8..fb551f326d 100644 --- a/app/src/main/kotlin/io/element/android/x/MainNode.kt +++ b/app/src/main/kotlin/io/element/android/x/MainNode.kt @@ -16,14 +16,17 @@ package io.element.android.x +import android.content.Intent import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode +import com.bumble.appyx.core.plugin.Plugin import io.element.android.appnav.LoggedInFlowNode import io.element.android.appnav.RoomFlowNode import io.element.android.appnav.RootFlowNode @@ -35,11 +38,13 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.x.di.MainDaggerComponentsOwner import io.element.android.x.di.RoomComponent import io.element.android.x.di.SessionComponent +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize class MainNode( buildContext: BuildContext, private val mainDaggerComponentOwner: MainDaggerComponentsOwner, + plugins: List, ) : ParentNode( navModel = PermanentNavModel( @@ -47,6 +52,7 @@ class MainNode( savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, + plugins = plugins, ), DaggerComponentOwner by mainDaggerComponentOwner { @@ -73,7 +79,13 @@ class MainNode( } override fun resolve(navTarget: RootNavTarget, buildContext: BuildContext): Node { - return createNode(buildContext, plugins = listOf(loggedInFlowNodeCallback, roomFlowNodeCallback)) + return createNode( + context = buildContext, + plugins = listOf( + loggedInFlowNodeCallback, + roomFlowNodeCallback, + ) + ) } @Composable @@ -81,6 +93,12 @@ class MainNode( Children(navModel = navModel) } + fun handleIntent(intent: Intent) { + lifecycleScope.launch { + waitForChildAttached().handleIntent(intent) + } + } + @Parcelize object RootNavTarget : Parcelable } 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 index b3c7aa98e0..e777b08906 100644 --- a/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt +++ b/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt @@ -18,7 +18,9 @@ package io.element.android.x.intent import android.content.Context import android.content.Intent +import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.deeplink.DeepLinkCreator import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.core.RoomId @@ -28,17 +30,19 @@ 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, + private val deepLinkCreator: DeepLinkCreator, ) : IntentProvider { - override fun getMainIntent(): Intent { - return Intent(context, MainActivity::class.java) - } - - override fun getIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): Intent { - // TODO Handle deeplink or pass parameters - return Intent(context, MainActivity::class.java) + override fun getViewIntent( + sessionId: SessionId, + roomId: RoomId?, + threadId: ThreadId?, + ): Intent { + return Intent(context, MainActivity::class.java).apply { + action = Intent.ACTION_VIEW + data = deepLinkCreator.create(sessionId, roomId, threadId).toUri() + } } } diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 17efdc15fc..b672f582f9 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) + implementation(projects.libraries.deeplink) implementation(projects.libraries.matrix.api) implementation(projects.libraries.push.api) implementation(projects.libraries.pushproviders.api) 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 6f9319f923..fe2d8aa0dc 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -33,6 +33,7 @@ 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 com.bumble.appyx.navmodel.backstack.operation.singleTop import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode @@ -56,10 +57,7 @@ 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.coroutines.runBlocking import kotlinx.parcelize.Parcelize -import kotlin.coroutines.coroutineContext @ContributesNode(AppScope::class) class LoggedInFlowNode @AssistedInject constructor( @@ -217,6 +215,19 @@ class LoggedInFlowNode @AssistedInject constructor( } } + suspend fun attachRoot(): Node { + return attachChild { + backstack.singleTop(NavTarget.RoomList) + } + } + + suspend fun attachRoom(roomId: RoomId): RoomFlowNode { + return attachChild { + backstack.singleTop(NavTarget.RoomList) + backstack.push(NavTarget.Room(roomId)) + } + } + @Composable override fun View(modifier: Modifier) { Box(modifier = modifier) { 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 69c02d500e..3609dbf57e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -18,7 +18,6 @@ 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 diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 8251584308..519c4e734b 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -17,6 +17,7 @@ package io.element.android.appnav import android.app.Activity +import android.content.Intent import android.os.Parcelable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -45,6 +46,8 @@ import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint 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.deeplink.DeeplinkData +import io.element.android.libraries.deeplink.DeeplinkParser import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService @@ -65,6 +68,7 @@ class RootFlowNode @AssistedInject constructor( private val matrixClientsHolder: MatrixClientsHolder, private val presenter: RootPresenter, private val bugReportEntryPoint: BugReportEntryPoint, + private val deeplinkParser: DeeplinkParser, ) : BackstackNode( backstack = BackStack( @@ -207,4 +211,30 @@ class RootFlowNode @AssistedInject constructor( CircularProgressIndicator() } } + + suspend fun handleIntent(intent: Intent) { + deeplinkParser.getFromIntent(intent) + ?.let { navigateTo(it) } + } + + private suspend fun navigateTo(deeplinkData: DeeplinkData) { + Timber.d("Navigating to $deeplinkData") + attachSession(deeplinkData.sessionId) + .apply { + val roomId = deeplinkData.roomId + if (roomId == null) { + // In case room is not provided, ensure the app navigate back to the room list + attachRoot() + } else { + attachRoom(roomId) + // TODO .attachThread(deeplinkData.threadId) + } + } + } + + private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode { + return attachChild { + backstack.newRoot(NavTarget.LoggedInFlow(sessionId)) + } + } } diff --git a/libraries/deeplink/build.gradle.kts b/libraries/deeplink/build.gradle.kts new file mode 100644 index 0000000000..5d28470cfc --- /dev/null +++ b/libraries/deeplink/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.deeplink" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.libraries.di) + implementation(libs.dagger) + implementation(libs.androidx.corektx) + implementation(projects.libraries.matrix.api) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) +} diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt new file mode 100644 index 0000000000..a135988ca0 --- /dev/null +++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.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.deeplink + +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.ThreadId +import javax.inject.Inject + +class DeepLinkCreator @Inject constructor() { + fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String { + return buildString { + append("elementx://open/") + append(sessionId.value) + if (roomId != null) { + append("/") + append(roomId.value) + if (threadId != null) { + append("/") + append(threadId.value) + } + } + } + } +} diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt new file mode 100644 index 0000000000..d393a37c16 --- /dev/null +++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.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.deeplink + +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.ThreadId + +data class DeeplinkData( + val sessionId: SessionId, + val roomId: RoomId? = null, + val threadId: ThreadId? = null, +) diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt new file mode 100644 index 0000000000..2affc50818 --- /dev/null +++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt @@ -0,0 +1,47 @@ +/* + * 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.deeplink + +import android.content.Intent +import android.net.Uri +import io.element.android.libraries.matrix.api.core.asRoomId +import io.element.android.libraries.matrix.api.core.asSessionId +import io.element.android.libraries.matrix.api.core.asThreadId +import javax.inject.Inject + +class DeeplinkParser @Inject constructor() { + fun getFromIntent(intent: Intent): DeeplinkData? { + return intent + .takeIf { it.action == Intent.ACTION_VIEW } + ?.data + ?.toDeeplinkData() + } + + private fun Uri.toDeeplinkData(): DeeplinkData? { + if (scheme != "elementx") return null + if (host != "open") return null + val pathBits = path.orEmpty().split("/").drop(1) + val sessionId = pathBits.elementAtOrNull(0)?.asSessionId() ?: return null + val roomId = pathBits.elementAtOrNull(1)?.asRoomId() + val threadId = pathBits.elementAtOrNull(2)?.asThreadId() + return DeeplinkData( + sessionId = sessionId, + roomId = roomId, + threadId = threadId, + ) + } +} 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 index 52abb3f6a4..ce2b1d3fce 100644 --- 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 @@ -25,9 +25,7 @@ interface IntentProvider { /** * Provide an intent to start the application. */ - fun getMainIntent(): Intent - - fun getIntent( + fun getViewIntent( sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?, 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 index 56054bbef8..bc20d49917 100644 --- 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 @@ -34,7 +34,6 @@ data class NotificationActionIds @Inject constructor( 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/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index add8fd74eb..8aeaa998ca 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 @@ -482,15 +482,11 @@ class NotificationUtils @Inject constructor( } private fun buildOpenRoomIntent(sessionId: SessionId, roomId: RoomId): PendingIntent? { - val roomIntent = intentProvider.getIntent(sessionId = sessionId, roomId = roomId, threadId = null) - roomIntent.action = actionIds.tapToView - // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that - roomIntent.data = createIgnoredUri("openRoom?$sessionId&$roomId") - + val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = roomId, threadId = null) return PendingIntent.getActivity( context, clock.epochMillis().toInt(), - roomIntent, + intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } @@ -498,22 +494,17 @@ class NotificationUtils @Inject constructor( private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: ThreadId?): PendingIntent? { val sessionId = roomInfo.sessionId val roomId = roomInfo.roomId - val threadIntentTap = intentProvider.getIntent(sessionId = sessionId, roomId = roomId, threadId = threadId) - 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?$sessionId&$roomId&$threadId") - + val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = roomId, threadId = threadId) return PendingIntent.getActivity( context, clock.epochMillis().toInt(), - threadIntentTap, + intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } private fun buildOpenHomePendingIntentForSummary(sessionId: SessionId): PendingIntent { - val intent = intentProvider.getIntent(sessionId = sessionId, roomId = null, threadId = null) - intent.data = createIgnoredUri("tapSummary?$sessionId") + val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = null, threadId = null) return PendingIntent.getActivity( context, clock.epochMillis().toInt(), diff --git a/tools/adb/deeplink.sh b/tools/adb/deeplink.sh new file mode 100755 index 0000000000..a88d1083b3 --- /dev/null +++ b/tools/adb/deeplink.sh @@ -0,0 +1,28 @@ +#! /bin/bash +# +# 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. +# + +# Format is: +# elementx://open/{sessionId} to open a session +# elementx://open/{sessionId}/{roomId} to open a room +# elementx://open/{sessionId}/{roomId}/{eventId} to open an event + +# Open a session +# adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org +# Open a room +adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org/!dehdDVSkabQLZFYrgo:matrix.org +# Open a thread +# adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org/!dehdDVSkabQLZFYrgo:matrix.org/\\\$threadId From da6a391cc6bdc4bef23062b66e6359515ca4ff1b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 13 Apr 2023 15:22:03 +0200 Subject: [PATCH 093/119] Add test for DeepLinkCreator --- libraries/deeplink/build.gradle.kts | 1 + .../android/libraries/deeplink/Constants.kt | 20 ++++++++++ .../libraries/deeplink/DeepLinkCreator.kt | 2 +- .../libraries/deeplink/DeeplinkParser.kt | 4 +- .../libraries/deeplink/DeepLinkCreatorTest.kt | 37 +++++++++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt create mode 100644 libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt diff --git a/libraries/deeplink/build.gradle.kts b/libraries/deeplink/build.gradle.kts index 5d28470cfc..a377d0c2a7 100644 --- a/libraries/deeplink/build.gradle.kts +++ b/libraries/deeplink/build.gradle.kts @@ -37,4 +37,5 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) + testImplementation(projects.libraries.matrix.test) } diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt new file mode 100644 index 0000000000..df26ef2fa0 --- /dev/null +++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt @@ -0,0 +1,20 @@ +/* + * 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.deeplink + +internal const val SCHEME = "elementx" +internal const val HOST = "open" diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt index a135988ca0..71aa7ebddd 100644 --- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt +++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt @@ -24,7 +24,7 @@ import javax.inject.Inject class DeepLinkCreator @Inject constructor() { fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String { return buildString { - append("elementx://open/") + append("$SCHEME://$HOST/") append(sessionId.value) if (roomId != null) { append("/") diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt index 2affc50818..9f217f497e 100644 --- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt +++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt @@ -32,8 +32,8 @@ class DeeplinkParser @Inject constructor() { } private fun Uri.toDeeplinkData(): DeeplinkData? { - if (scheme != "elementx") return null - if (host != "open") return null + if (scheme != SCHEME) return null + if (host != HOST) return null val pathBits = path.orEmpty().split("/").drop(1) val sessionId = pathBits.elementAtOrNull(0)?.asSessionId() ?: return null val roomId = pathBits.elementAtOrNull(1)?.asRoomId() diff --git a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt new file mode 100644 index 0000000000..730bdde248 --- /dev/null +++ b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt @@ -0,0 +1,37 @@ +/* + * 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.deeplink + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_THREAD_ID +import org.junit.Test + +class DeepLinkCreatorTest { + + @Test + fun create() { + val sut = DeepLinkCreator() + assertThat(sut.create(A_SESSION_ID, null, null)) + .isEqualTo("elementx://open/@alice:server.org") + assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, null)) + .isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain") + assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID)) + .isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId") + } +} From 54659083653c374e56aa57bf017b588866a36bc7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 13 Apr 2023 15:38:54 +0200 Subject: [PATCH 094/119] Add test for DeeplinkParser --- libraries/deeplink/build.gradle.kts | 2 + .../libraries/deeplink/DeeplinkParserTest.kt | 78 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt diff --git a/libraries/deeplink/build.gradle.kts b/libraries/deeplink/build.gradle.kts index a377d0c2a7..3fe27bfd1c 100644 --- a/libraries/deeplink/build.gradle.kts +++ b/libraries/deeplink/build.gradle.kts @@ -37,5 +37,7 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) + testImplementation(libs.test.robolectric) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) } diff --git a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt new file mode 100644 index 0000000000..259cd6fde1 --- /dev/null +++ b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt @@ -0,0 +1,78 @@ +/* + * 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.deeplink + +import android.content.Intent +import androidx.core.net.toUri +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_THREAD_ID +import io.element.android.tests.testutils.assertNullOrThrow +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DeeplinkParserTest { + companion object { + const val A_URI = + "elementx://open/@alice:server.org" + const val A_URI_WITH_ROOM = + "elementx://open/@alice:server.org/!aRoomId:domain" + const val A_URI_WITH_ROOM_WITH_THREAD = + "elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId" + } + + private val sut = DeeplinkParser() + + @Test + fun `nominal cases`() { + assertThat(sut.getFromIntent(createIntent(A_URI))) + .isEqualTo(DeeplinkData(A_SESSION_ID, null, null)) + assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM))) + .isEqualTo(DeeplinkData(A_SESSION_ID, A_ROOM_ID, null)) + assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM_WITH_THREAD))) + .isEqualTo(DeeplinkData(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID)) + } + + @Test + fun `error cases`() { + val sut = DeeplinkParser() + // Bad scheme + assertThat(sut.getFromIntent(createIntent("x://open/@alice:server.org"))).isNull() + // Bad host + assertThat(sut.getFromIntent(createIntent("elementx://close/@alice:server.org"))).isNull() + // No session Id + assertThat(sut.getFromIntent(createIntent("elementx://open"))).isNull() + // Invalid sessionId + assertNullOrThrow { + sut.getFromIntent(createIntent("elementx://open/alice:server.org")) + } + // Empty sessionId + assertNullOrThrow { + sut.getFromIntent(createIntent("elementx://open//")) + } + } + + private fun createIntent(uri: String): Intent { + return Intent().apply { + action = Intent.ACTION_VIEW + data = uri.toUri() + } + } +} From 0a926bd05ae0c470e5f21a6aa924128410e223df Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 3 Apr 2023 09:28:24 +0200 Subject: [PATCH 095/119] Navigate from people view to configuration view --- .../createroom/impl/CreateRoomFlowNode.kt | 21 +++++-- .../impl/addpeople/AddPeopleNode.kt | 12 +++- .../impl/addpeople/AddPeopleView.kt | 5 +- .../impl/configureroom/ConfigureRoomEvents.kt | 22 +++++++ .../impl/configureroom/ConfigureRoomNode.kt | 56 ++++++++++++++++++ .../configureroom/ConfigureRoomPresenter.kt | 48 +++++++++++++++ .../ConfigureRoomPresenterArgs.kt | 23 ++++++++ .../impl/configureroom/ConfigureRoomState.kt | 24 ++++++++ .../ConfigureRoomStateProvider.kt | 32 ++++++++++ .../impl/configureroom/ConfigureRoomView.kt | 58 +++++++++++++++++++ libraries/designsystem/build.gradle.kts | 1 + .../components/avatar/AvatarData.kt | 7 ++- libraries/matrixui/build.gradle.kts | 1 + .../libraries/matrix/ui/model/MatrixUser.kt | 5 +- 14 files changed, 306 insertions(+), 9 deletions(-) create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt 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 22851904c4..137017a2ec 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 @@ -31,12 +31,14 @@ 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.configureroom.ConfigureRoomNode 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 io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) @@ -58,12 +60,15 @@ class CreateRoomFlowNode @AssistedInject constructor( @Parcelize object NewRoom : NavTarget + + @Parcelize + data class ConfigureRoom(val users: List) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Root -> { - val callback = object : CreateRoomRootNode.Callback { + createNode(context = buildContext, plugins = listOf(object : CreateRoomRootNode.Callback { override fun onCreateNewRoom() { backstack.push(NavTarget.NewRoom) } @@ -71,10 +76,18 @@ class CreateRoomFlowNode @AssistedInject constructor( override fun onOpenRoom(roomId: RoomId) { plugins().forEach { it.onOpenRoom(roomId) } } - } - createNode(buildContext, plugins = listOf(callback)) + })) + } + NavTarget.NewRoom -> { + createNode(context = buildContext, plugins = listOf(object : AddPeopleNode.Callback { + override fun onContinue(selectedUsers: List) { + backstack.push(NavTarget.ConfigureRoom(selectedUsers)) + } + })) + } + is NavTarget.ConfigureRoom -> { + createNode(context = buildContext, plugins = listOf(ConfigureRoomNode.Inputs(navTarget.users))) } - NavTarget.NewRoom -> createNode(buildContext) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt index 5393075d18..91b1d5a721 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt @@ -21,10 +21,12 @@ 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 com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.ui.model.MatrixUser @ContributesNode(SessionScope::class) class AddPeopleNode @AssistedInject constructor( @@ -33,6 +35,14 @@ class AddPeopleNode @AssistedInject constructor( private val presenter: AddPeoplePresenter, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onContinue(selectedUsers: List) + } + + private fun onContinue(selectedUsers: List) { + plugins().forEach { it.onContinue(selectedUsers) } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -40,7 +50,7 @@ class AddPeopleNode @AssistedInject constructor( state = state, modifier = modifier, onBackPressed = { navigateUp() }, - onNextPressed = { }, + onNextPressed = this::onContinue, ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt index 56a16b24f9..e74a1477d2 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -38,6 +38,7 @@ import io.element.android.libraries.designsystem.theme.components.CenterAlignedT 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.TextButton +import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.ui.strings.R as StringR @OptIn(ExperimentalMaterial3Api::class) @@ -46,7 +47,7 @@ fun AddPeopleView( state: AddPeopleState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, - onNextPressed: () -> Unit = {}, + onNextPressed: (List) -> Unit = {}, ) { val eventSink = state.eventSink @@ -56,7 +57,7 @@ fun AddPeopleView( AddPeopleViewTopBar( hasSelectedUsers = state.userListState.selectedUsers.isNotEmpty(), onBackPressed = onBackPressed, - onNextPressed = onNextPressed, + onNextPressed = { onNextPressed(state.selectUsersState.selectedUsers) }, ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt new file mode 100644 index 0000000000..d1ba2c8ebe --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.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.features.createroom.impl.configureroom + +// TODO Add your events or remove the file completely if no events +sealed interface ConfigureRoomEvents { + object MyEvent : ConfigureRoomEvents +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt new file mode 100644 index 0000000000..6e11e492b8 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.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.features.createroom.impl.configureroom + +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.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.ui.model.MatrixUser + +@ContributesNode(SessionScope::class) +class ConfigureRoomNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenterFactory: ConfigureRoomPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val selectedUsers: List + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val presenter by lazy { + presenterFactory.create(ConfigureRoomPresenterArgs(inputs.selectedUsers)) + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ConfigureRoomView( + state = state, + modifier = modifier, + ) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt new file mode 100644 index 0000000000..243769e90d --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.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.features.createroom.impl.configureroom + +import androidx.compose.runtime.Composable +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Presenter + +class ConfigureRoomPresenter @AssistedInject constructor( + @Assisted val args: ConfigureRoomPresenterArgs, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(args: ConfigureRoomPresenterArgs): ConfigureRoomPresenter + } + + @Composable + override fun present(): ConfigureRoomState { + + fun handleEvents(event: ConfigureRoomEvents) { + when (event) { + ConfigureRoomEvents.MyEvent -> Unit + } + } + + return ConfigureRoomState( + selectedUsers = args.selectedUsers, + eventSink = ::handleEvents, + ) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.kt new file mode 100644 index 0000000000..8969a2a5fa --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.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.features.createroom.impl.configureroom + +import io.element.android.libraries.matrix.ui.model.MatrixUser + +data class ConfigureRoomPresenterArgs( + val selectedUsers: List, +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt new file mode 100644 index 0000000000..b06f4e29b9 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.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.features.createroom.impl.configureroom + +import io.element.android.libraries.matrix.ui.model.MatrixUser + +data class ConfigureRoomState( + val selectedUsers: List, + val eventSink: (ConfigureRoomEvents) -> Unit +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt new file mode 100644 index 0000000000..f9701fdaa6 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.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.features.createroom.impl.configureroom + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class ConfigureRoomStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aConfigureRoomState(), + // Add other state here + ) +} + +fun aConfigureRoomState() = ConfigureRoomState( + selectedUsers = emptyList(), + eventSink = {} +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt new file mode 100644 index 0000000000..43c4a84f03 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -0,0 +1,58 @@ +/* + * 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.createroom.impl.configureroom + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +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.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun ConfigureRoomView( + state: ConfigureRoomState, + modifier: Modifier = Modifier, +) { + Box(modifier, contentAlignment = Alignment.Center) { + Text( + "ConfigureRoom feature view", + color = MaterialTheme.colorScheme.primary, + ) + } +} + +@Preview +@Composable +fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: ConfigureRoomState) { + ConfigureRoomView( + state = state, + ) +} diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index 45430e5d82..4533c950b1 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -19,6 +19,7 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.ksp) + id("kotlin-parcelize") } android { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt index 7f2cddea09..3bf4f7d0b4 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt @@ -16,15 +16,20 @@ package io.element.android.libraries.designsystem.components.avatar +import android.os.Parcelable import androidx.compose.runtime.Immutable +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize @Immutable +@Parcelize data class AvatarData( val id: String, val name: String?, val url: String? = null, + @IgnoredOnParcel val size: AvatarSize = AvatarSize.MEDIUM -) { +) : Parcelable { fun getInitial(): String { val firstChar = name?.firstOrNull() ?: id.getOrNull(1) ?: '?' return firstChar.uppercase() diff --git a/libraries/matrixui/build.gradle.kts b/libraries/matrixui/build.gradle.kts index c9dead5eb4..6d2109d3f8 100644 --- a/libraries/matrixui/build.gradle.kts +++ b/libraries/matrixui/build.gradle.kts @@ -20,6 +20,7 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.anvil) alias(libs.plugins.ksp) + id("kotlin-parcelize") } android { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUser.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUser.kt index c524cbb733..f0ddb30a93 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUser.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUser.kt @@ -16,16 +16,19 @@ package io.element.android.libraries.matrix.ui.model +import android.os.Parcelable import androidx.compose.runtime.Immutable import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.parcelize.Parcelize +@Parcelize @Immutable data class MatrixUser( val id: UserId, val username: String? = null, val avatarData: AvatarData = AvatarData(id.value, username), -) +) : Parcelable fun MatrixUser.getBestName(): String { return username?.takeIf { it.isNotEmpty() } ?: id.value From ac5f50d264a8899c52043cb7b45dd7075103ea38 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 3 Apr 2023 15:02:33 +0200 Subject: [PATCH 096/119] WIP create room screen --- features/createroom/impl/build.gradle.kts | 1 + .../configureroom/ConfigureRoomPresenter.kt | 4 +- .../impl/configureroom/ConfigureRoomState.kt | 5 +- .../ConfigureRoomStateProvider.kt | 5 +- .../impl/configureroom/ConfigureRoomView.kt | 195 +++++++++++++++++- .../features/userlist/api/UserListView.kt | 3 + 6 files changed, 207 insertions(+), 6 deletions(-) diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 6f2544822c..77a5ed26be 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.features.userlist.api) api(projects.features.createroom.api) + implementation(libs.coil.compose) // FIXME temp testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 243769e90d..25851e41af 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -21,6 +21,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.libraries.architecture.Presenter +import kotlinx.collections.immutable.toImmutableList class ConfigureRoomPresenter @AssistedInject constructor( @Assisted val args: ConfigureRoomPresenterArgs, @@ -41,7 +42,8 @@ class ConfigureRoomPresenter @AssistedInject constructor( } return ConfigureRoomState( - selectedUsers = args.selectedUsers, + selectedUsers = args.selectedUsers.toImmutableList(), + avatarUri = null, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index b06f4e29b9..b8eee9e7bf 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -16,9 +16,12 @@ package io.element.android.features.createroom.impl.configureroom +import android.net.Uri import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.ImmutableList data class ConfigureRoomState( - val selectedUsers: List, + val selectedUsers: ImmutableList, + val avatarUri: Uri?, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index f9701fdaa6..30100c720b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -17,6 +17,8 @@ package io.element.android.features.createroom.impl.configureroom import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import kotlinx.collections.immutable.persistentListOf open class ConfigureRoomStateProvider : PreviewParameterProvider { override val values: Sequence @@ -27,6 +29,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider Unit = {}, + onCreatePressed: () -> Unit = {}, ) { - Box(modifier, contentAlignment = Alignment.Center) { + Scaffold( + modifier = modifier, + topBar = { + ConfigureRoomToolbar( + isNextActionEnabled = false, + onBackPressed = onBackPressed, + onNextPressed = onCreatePressed, + ) + } + ) { padding -> + Column( + modifier = Modifier.padding(padding), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + RoomNameWithAvatar( + modifier = Modifier.padding(horizontal = 16.dp), + ) + RoomTopic( + modifier = Modifier.padding(horizontal = 16.dp), + ) + SelectedUsersList( + listState = LazyListState(), // FIXME + contentPadding = PaddingValues(horizontal = 24.dp), + selectedUsers = state.selectedUsers, + onUserRemoved = { + // TODO + }, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ConfigureRoomToolbar( + isNextActionEnabled: Boolean, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, + onNextPressed: () -> Unit = {}, +) { + CenterAlignedTopAppBar( + modifier = modifier, + title = { + Text( + text = "Create a room", + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ) + }, + navigationIcon = { BackButton(onClick = onBackPressed) }, + actions = { + TextButton( + modifier = Modifier.padding(horizontal = 8.dp), + enabled = isNextActionEnabled, + onClick = onNextPressed, + ) { + Text( + text = "Create", + fontSize = 16.sp, + ) + } + } + ) +} + +@Composable +fun RoomNameWithAvatar( + modifier: Modifier = Modifier, + avatarUri: Uri? = null, + roomName: String = "", + onAvatarClick: () -> Unit = {}, + onRoomNameChanged: (String) -> Unit = {}, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Avatar( + avatarUri = avatarUri, + onClick = onAvatarClick, + ) + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = "Room name" + ) + + TextField( + modifier = Modifier.fillMaxWidth(), + value = roomName, + placeholder = { Text("e.g. Product Sprint") }, + onValueChange = onRoomNameChanged, + maxLines = 1, + ) + } + } +} + +@Composable +fun Avatar( + modifier: Modifier = Modifier, + avatarUri: Uri? = null, + onClick: () -> Unit = {}, +) { + val commonModifier = modifier + .size(70.dp) + .clip(CircleShape) + .clickable(onClick = onClick) + + if (avatarUri != null) { + val context = LocalContext.current + val model = ImageRequest.Builder(context) + .data(avatarUri) + .build() + AsyncImage( + model = model, + contentDescription = null, + modifier = commonModifier, + ) + } else { + Box( + modifier = commonModifier + .background(LocalColors.current.quinary) + ) { + Icon( + imageVector = Icons.Outlined.AddAPhoto, + contentDescription = "", + modifier = modifier + .align(Alignment.Center) + .size(40.dp), + tint = MaterialTheme.colorScheme.secondary, + ) + } + } +} + +@Composable +fun RoomTopic( + modifier: Modifier = Modifier, + topic: String = "", + onTopicChanged: (String) -> Unit = {}, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { Text( - "ConfigureRoom feature view", - color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(horizontal = 16.dp), + text = "Topic (optional)", + ) + TextField( + modifier = Modifier.fillMaxWidth(), + value = topic, + placeholder = { Text("What is this room about?") }, + onValueChange = onTopicChanged, + maxLines = 3, ) } } diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt index bc355a0a26..5605ed6028 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -244,11 +245,13 @@ fun SelectedUsersList( listState: LazyListState, selectedUsers: ImmutableList, modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp), onUserRemoved: (MatrixUser) -> Unit = {}, ) { LazyRow( state = listState, modifier = modifier, + contentPadding = contentPadding, horizontalArrangement = Arrangement.spacedBy(24.dp), ) { items(selectedUsers.toList()) { matrixUser -> From 6c4cc71d3f6164ff70695fb9288c314942cc8e90 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 3 Apr 2023 15:11:22 +0200 Subject: [PATCH 097/119] Use content padding --- .../io/element/android/features/userlist/api/UserListView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt index 5605ed6028..5f587b471e 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt @@ -98,7 +98,7 @@ fun UserListView( if (state.isMultiSelectionEnabled && !state.isSearchActive && state.selectedUsers.isNotEmpty()) { SelectedUsersList( listState = state.selectedUsersListState, - modifier = Modifier.padding(16.dp), + contentPadding = PaddingValues(16.dp), selectedUsers = state.selectedUsers, onUserRemoved = { state.eventSink(UserListEvents.RemoveFromSelection(it)) @@ -175,7 +175,7 @@ fun SearchUserBar( if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) { SelectedUsersList( listState = selectedUsersListState, - modifier = Modifier.padding(16.dp), + contentPadding = PaddingValues(16.dp), selectedUsers = selectedUsers, onUserRemoved = onUserDeselected, ) From 66b672e6554a339412fb57a94c1ea64f6010a109 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 4 Apr 2023 16:42:06 +0200 Subject: [PATCH 098/119] Change wording of create a room button --- .../features/createroom/impl/root/CreateRoomRootView.kt | 2 +- ...CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...reateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- 3 files changed, 5 insertions(+), 5 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 e488645b78..4e12dcaab0 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 @@ -156,7 +156,7 @@ fun CreateRoomActionButtonsList( Column(modifier = modifier) { CreateRoomActionButton( iconRes = DrawableR.drawable.ic_groups, - text = stringResource(id = R.string.screen_create_room_action_create_room), + text = stringResource(id = StringR.string.action_create_a_room), onClick = onNewRoomClicked, ) CreateRoomActionButton( 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_0,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_0,NEXUS_5,1.0,en].png index 1c22798a9a..de4377ecc9 100644 --- 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_0,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_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:365161fefa9ea3c82bf3ed2527b4847df27860266e1d5f0e770962e95154b4a6 -size 19178 +oid sha256:6f36c2b4f4266048d5295df08f380e4128630d11cce7ac11cf3a0eaaa5594d61 +size 19924 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_0,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_0,NEXUS_5,1.0,en].png index d404485dee..0bdbed7968 100644 --- 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_0,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_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f4413c59f47c45c06433f53c3f1fb7e9095bb47e3d8c3e7fabefcb19cdd5146 -size 18346 +oid sha256:23b9e996b8f0cc2efcb3adb6294bfa7b4a53fefbb7b6ee07add4105da9b9d40e +size 18984 From 97ade693f54eae2881941ac933a2bbcbd126e50a Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 4 Apr 2023 17:26:00 +0200 Subject: [PATCH 099/119] Add fake list of matrix users --- .../configureroom/ConfigureRoomStateProvider.kt | 6 +++--- .../matrix/ui/components/MatrixUserProvider.kt | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 30100c720b..2bc7f62ee8 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -17,8 +17,8 @@ package io.element.android.features.createroom.impl.configureroom import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.matrix.ui.components.aMatrixUser -import kotlinx.collections.immutable.persistentListOf +import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import kotlinx.collections.immutable.toImmutableList open class ConfigureRoomStateProvider : PreviewParameterProvider { override val values: Sequence @@ -29,7 +29,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( From 470afea802c9d1a4583213aff5a92053fe516fcb Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 4 Apr 2023 17:41:31 +0200 Subject: [PATCH 100/119] Add topic and room name to the state --- .../impl/configureroom/ConfigureRoomEvents.kt | 7 +++++-- .../impl/configureroom/ConfigureRoomPresenter.kt | 16 ++++++++++++++-- .../impl/configureroom/ConfigureRoomState.kt | 2 ++ .../configureroom/ConfigureRoomStateProvider.kt | 2 ++ .../impl/configureroom/ConfigureRoomView.kt | 13 +++++++++---- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt index d1ba2c8ebe..db90c58834 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -16,7 +16,10 @@ package io.element.android.features.createroom.impl.configureroom -// TODO Add your events or remove the file completely if no events +import android.net.Uri + sealed interface ConfigureRoomEvents { - object MyEvent : ConfigureRoomEvents + data class RoomNameChanged(val name: String) : ConfigureRoomEvents + data class TopicChanged(val topic: String) : ConfigureRoomEvents + data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 25851e41af..56962647ad 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -16,7 +16,12 @@ package io.element.android.features.createroom.impl.configureroom +import android.net.Uri import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -34,16 +39,23 @@ class ConfigureRoomPresenter @AssistedInject constructor( @Composable override fun present(): ConfigureRoomState { + var roomName by rememberSaveable { mutableStateOf("") } + var topic by rememberSaveable { mutableStateOf("") } + var avatarUri by rememberSaveable { mutableStateOf(null) } fun handleEvents(event: ConfigureRoomEvents) { when (event) { - ConfigureRoomEvents.MyEvent -> Unit + is ConfigureRoomEvents.AvatarUriChanged -> avatarUri = event.uri + is ConfigureRoomEvents.RoomNameChanged -> roomName = event.name + is ConfigureRoomEvents.TopicChanged -> topic = event.topic } } return ConfigureRoomState( selectedUsers = args.selectedUsers.toImmutableList(), - avatarUri = null, + roomName = roomName, + topic = topic, + avatarUri = avatarUri, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index b8eee9e7bf..d8a3635d35 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -22,6 +22,8 @@ import kotlinx.collections.immutable.ImmutableList data class ConfigureRoomState( val selectedUsers: ImmutableList, + val roomName: String, + val topic: String, val avatarUri: Uri?, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 2bc7f62ee8..2838d77f23 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -30,6 +30,8 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider Unit = {}, onRoomNameChanged: (String) -> Unit = {}, ) { @@ -170,7 +175,7 @@ fun RoomNameWithAvatar( @Composable fun Avatar( modifier: Modifier = Modifier, - avatarUri: Uri? = null, + avatarUri: Uri?, onClick: () -> Unit = {}, ) { val commonModifier = modifier @@ -208,7 +213,7 @@ fun Avatar( @Composable fun RoomTopic( modifier: Modifier = Modifier, - topic: String = "", + topic: String, onTopicChanged: (String) -> Unit = {}, ) { Column( From 5e88b2337280a3d55e6575644737742dcf67d5f0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 16:28:55 +0200 Subject: [PATCH 101/119] Fix build --- .../features/createroom/impl/addpeople/AddPeopleView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt index e74a1477d2..eed59923a8 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -29,8 +29,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import io.element.android.features.userlist.api.UserListView import io.element.android.features.createroom.impl.R +import io.element.android.features.userlist.api.UserListView import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -57,7 +57,7 @@ fun AddPeopleView( AddPeopleViewTopBar( hasSelectedUsers = state.userListState.selectedUsers.isNotEmpty(), onBackPressed = onBackPressed, - onNextPressed = { onNextPressed(state.selectUsersState.selectedUsers) }, + onNextPressed = { onNextPressed(state.userListState.selectedUsers) }, ) } } From bb48f5f378c1d24d172366e2b21332711dbbffec Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 16:28:09 +0200 Subject: [PATCH 102/119] Room visibility --- .../impl/configureroom/ConfigureRoomEvents.kt | 1 + .../configureroom/ConfigureRoomPresenter.kt | 3 + .../impl/configureroom/ConfigureRoomState.kt | 1 + .../ConfigureRoomStateProvider.kt | 1 + .../impl/configureroom/ConfigureRoomView.kt | 125 +++++++++++++++++- .../impl/configureroom/RoomPrivacy.kt | 22 +++ .../theme/components/RadioButton.kt | 63 +++++++++ 7 files changed, 209 insertions(+), 7 deletions(-) create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt index db90c58834..7322dbd961 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -22,4 +22,5 @@ sealed interface ConfigureRoomEvents { data class RoomNameChanged(val name: String) : ConfigureRoomEvents data class TopicChanged(val topic: String) : ConfigureRoomEvents data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents + data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 56962647ad..97871fb99a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -42,12 +42,14 @@ class ConfigureRoomPresenter @AssistedInject constructor( var roomName by rememberSaveable { mutableStateOf("") } var topic by rememberSaveable { mutableStateOf("") } var avatarUri by rememberSaveable { mutableStateOf(null) } + var privacy by rememberSaveable { mutableStateOf(null) } fun handleEvents(event: ConfigureRoomEvents) { when (event) { is ConfigureRoomEvents.AvatarUriChanged -> avatarUri = event.uri is ConfigureRoomEvents.RoomNameChanged -> roomName = event.name is ConfigureRoomEvents.TopicChanged -> topic = event.topic + is ConfigureRoomEvents.RoomPrivacyChanged -> privacy = event.privacy } } @@ -56,6 +58,7 @@ class ConfigureRoomPresenter @AssistedInject constructor( roomName = roomName, topic = topic, avatarUri = avatarUri, + privacy = privacy, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index d8a3635d35..643f01ef3e 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -25,5 +25,6 @@ data class ConfigureRoomState( val roomName: String, val topic: String, val avatarUri: Uri?, + val privacy: RoomPrivacy?, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 2838d77f23..d097b0e599 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -33,5 +33,6 @@ fun aConfigureRoomState() = ConfigureRoomState( roomName = "", topic = "", avatarUri = null, + privacy = null, eventSink = {} ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 360ad2e0e0..d492d5d81c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -24,20 +24,27 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AddAPhoto +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.Public import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -45,13 +52,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import coil.request.ImageRequest -import io.element.android.features.selectusers.api.SelectedUsersList +import io.element.android.features.userlist.api.SelectedUsersList import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.LocalColors import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.RadioButton 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.TextButton @@ -98,6 +106,12 @@ fun ConfigureRoomView( // TODO }, ) + Spacer(Modifier.weight(1f)) + RoomPrivacyOptions( + modifier = Modifier.padding(bottom = 40.dp), + selected = state.privacy, + onOptionSelected = { state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it)) }, + ) } } } @@ -189,15 +203,12 @@ fun Avatar( .data(avatarUri) .build() AsyncImage( + modifier = commonModifier, model = model, contentDescription = null, - modifier = commonModifier, ) } else { - Box( - modifier = commonModifier - .background(LocalColors.current.quinary) - ) { + Box(modifier = commonModifier.background(LocalColors.current.quinary)) { Icon( imageVector = Icons.Outlined.AddAPhoto, contentDescription = "", @@ -212,8 +223,8 @@ fun Avatar( @Composable fun RoomTopic( - modifier: Modifier = Modifier, topic: String, + modifier: Modifier = Modifier, onTopicChanged: (String) -> Unit = {}, ) { Column( @@ -234,6 +245,106 @@ fun RoomTopic( } } +@Composable +fun RoomPrivacyOptions( + selected: RoomPrivacy?, + modifier: Modifier = Modifier, + onOptionSelected: (RoomPrivacy) -> Unit = {}, +) { + + data class RoomPrivacyItem( + val privacy: RoomPrivacy, + val icon: ImageVector, + val title: String, + val description: String, + ) + + val items = RoomPrivacy.values().map { + when (it) { + RoomPrivacy.Public -> RoomPrivacyItem( + privacy = it, + icon = Icons.Outlined.Lock, + title = "Private room (invite only)", + description = "Messages in this room are encrypted. Encryption can’t be disabled afterwards.", + ) + RoomPrivacy.Private -> RoomPrivacyItem( + privacy = it, + icon = Icons.Outlined.Public, + title = "Public room (anyone)", + description = "Messages are not encrypted and anyone can read them. You can enable encryption at a later date.", + ) + } + } + Column(modifier = modifier.selectableGroup()) { + items.forEach { item -> + RoomPrivacyOption( + privacy = RoomPrivacy.Private, + icon = item.icon, + title = item.title, + description = item.description, + isSelected = selected == item.privacy, + onOptionSelected = { onOptionSelected(item.privacy) } + ) + } + } +} + +@Composable +fun RoomPrivacyOption( + privacy: RoomPrivacy, + icon: ImageVector, + title: String, + description: String, + modifier: Modifier = Modifier, + isSelected: Boolean = false, + onOptionSelected: (RoomPrivacy) -> Unit = {}, +) { + Row( + modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + onClick = { onOptionSelected(privacy) }, + role = Role.RadioButton, + ) + .padding(8.dp), + ) { + Icon( + modifier = Modifier.padding(horizontal = 8.dp), + imageVector = icon, + contentDescription = "", + tint = MaterialTheme.colorScheme.secondary, + ) + + Column( + Modifier + .weight(1f) + .padding(horizontal = 8.dp) + ) { + Text( + text = title, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(Modifier.size(3.dp)) + Text( + text = description, + fontSize = 12.sp, + lineHeight = 17.sp, + color = MaterialTheme.colorScheme.tertiary, + ) + } + + RadioButton( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(48.dp), + selected = isSelected, + onClick = null // null recommended for accessibility with screenreaders + ) + } +} + @Preview @Composable fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt new file mode 100644 index 0000000000..e0b7411680 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.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.features.createroom.impl.configureroom + +enum class RoomPrivacy { + Public, + Private, +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt new file mode 100644 index 0000000000..3e703962a0 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.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.designsystem.theme.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.RadioButtonColors +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight + +@Composable +fun RadioButton( + selected: Boolean, + onClick: (() -> Unit)?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: RadioButtonColors = RadioButtonDefaults.colors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + androidx.compose.material3.RadioButton( + selected = selected, + onClick = onClick, + modifier = modifier, + enabled = enabled, + colors = colors, + interactionSource = interactionSource, + ) +} + +@Preview +@Composable +internal fun RadioButtonLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun RadioButtonDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + RadioButton(selected = false, onClick = {}) + RadioButton(selected = true, onClick = {}) + } +} From 11994ec629386a288093beb130076db19d8b4255 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 14:29:36 +0200 Subject: [PATCH 103/119] Extract room name and topic to dedicated composable --- .../impl/configureroom/ConfigureRoomView.kt | 72 +++++++++++-------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index d492d5d81c..50514b663a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -167,22 +167,12 @@ fun RoomNameWithAvatar( onClick = onAvatarClick, ) - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = "Room name" - ) - - TextField( - modifier = Modifier.fillMaxWidth(), - value = roomName, - placeholder = { Text("e.g. Product Sprint") }, - onValueChange = onRoomNameChanged, - maxLines = 1, - ) - } + LabelledTextField( + label = "Room name", + value = roomName, + placeholder = "e.g. Product Sprint", + onValueChange = onRoomNameChanged + ) } } @@ -227,22 +217,14 @@ fun RoomTopic( modifier: Modifier = Modifier, onTopicChanged: (String) -> Unit = {}, ) { - Column( + LabelledTextField( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = "Topic (optional)", - ) - TextField( - modifier = Modifier.fillMaxWidth(), - value = topic, - placeholder = { Text("What is this room about?") }, - onValueChange = onTopicChanged, - maxLines = 3, - ) - } + label = "Topic (optional)", + value = topic, + placeholder = "What is this room about?", + onValueChange = onTopicChanged, + maxLines = 3, + ) } @Composable @@ -345,6 +327,34 @@ fun RoomPrivacyOption( } } +@Composable +fun LabelledTextField( + label: String, + value: String, + modifier: Modifier = Modifier, + placeholder: String = "", + maxLines: Int = 1, + onValueChange: (String) -> Unit, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = label + ) + + TextField( + modifier = Modifier.fillMaxWidth(), + value = value, + placeholder = { Text(placeholder) }, + onValueChange = onValueChange, + maxLines = maxLines, + ) + } +} + @Preview @Composable fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = From f1b350c8d4e2e746f4cc9d9a88f2f3421e01aa8d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 14:34:24 +0200 Subject: [PATCH 104/119] Use rememberLazyListState --- .../createroom/impl/configureroom/ConfigureRoomView.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 50514b663a..5b9b548ea3 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -28,7 +28,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape @@ -73,6 +73,7 @@ fun ConfigureRoomView( onBackPressed: () -> Unit = {}, onCreatePressed: () -> Unit = {}, ) { + val selectedUsersListState = rememberLazyListState() Scaffold( modifier = modifier, topBar = { @@ -99,12 +100,10 @@ fun ConfigureRoomView( onTopicChanged = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) }, ) SelectedUsersList( - listState = LazyListState(), // FIXME + listState = selectedUsersListState, contentPadding = PaddingValues(horizontal = 24.dp), selectedUsers = state.selectedUsers, - onUserRemoved = { - // TODO - }, + onUserRemoved = { }, // TODO ) Spacer(Modifier.weight(1f)) RoomPrivacyOptions( From 5741be3689832b5911c04c6275536e37774b8e66 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 14:51:47 +0200 Subject: [PATCH 105/119] Update enable state of create room button --- .../createroom/impl/configureroom/ConfigureRoomEvents.kt | 1 + .../createroom/impl/configureroom/ConfigureRoomPresenter.kt | 6 ++++++ .../createroom/impl/configureroom/ConfigureRoomState.kt | 1 + .../impl/configureroom/ConfigureRoomStateProvider.kt | 2 +- .../createroom/impl/configureroom/ConfigureRoomView.kt | 6 +++--- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt index 7322dbd961..644c3d1e03 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -23,4 +23,5 @@ sealed interface ConfigureRoomEvents { data class TopicChanged(val topic: String) : ConfigureRoomEvents data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents + object CreateRoom : ConfigureRoomEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 97871fb99a..c8bad29f93 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -43,6 +43,10 @@ class ConfigureRoomPresenter @AssistedInject constructor( var topic by rememberSaveable { mutableStateOf("") } var avatarUri by rememberSaveable { mutableStateOf(null) } var privacy by rememberSaveable { mutableStateOf(null) } + val isCreateButtonEnabled by rememberSaveable(roomName, privacy) { + val enabled = roomName.isNotEmpty() && privacy != null + mutableStateOf(enabled) + } fun handleEvents(event: ConfigureRoomEvents) { when (event) { @@ -50,6 +54,7 @@ class ConfigureRoomPresenter @AssistedInject constructor( is ConfigureRoomEvents.RoomNameChanged -> roomName = event.name is ConfigureRoomEvents.TopicChanged -> topic = event.topic is ConfigureRoomEvents.RoomPrivacyChanged -> privacy = event.privacy + ConfigureRoomEvents.CreateRoom -> Unit // TODO } } @@ -59,6 +64,7 @@ class ConfigureRoomPresenter @AssistedInject constructor( topic = topic, avatarUri = avatarUri, privacy = privacy, + isCreateButtonEnabled = isCreateButtonEnabled, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index 643f01ef3e..23f5691267 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -26,5 +26,6 @@ data class ConfigureRoomState( val topic: String, val avatarUri: Uri?, val privacy: RoomPrivacy?, + val isCreateButtonEnabled: Boolean, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index d097b0e599..3e4961f56a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -24,7 +24,6 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider get() = sequenceOf( aConfigureRoomState(), - // Add other state here ) } @@ -34,5 +33,6 @@ fun aConfigureRoomState() = ConfigureRoomState( topic = "", avatarUri = null, privacy = null, + isCreateButtonEnabled = false, eventSink = {} ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 5b9b548ea3..6f56ae06ef 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -71,16 +71,15 @@ fun ConfigureRoomView( state: ConfigureRoomState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, - onCreatePressed: () -> Unit = {}, ) { val selectedUsersListState = rememberLazyListState() Scaffold( modifier = modifier, topBar = { ConfigureRoomToolbar( - isNextActionEnabled = false, + isNextActionEnabled = state.isCreateButtonEnabled, onBackPressed = onBackPressed, - onNextPressed = onCreatePressed, + onNextPressed = { state.eventSink(ConfigureRoomEvents.CreateRoom) }, ) } ) { padding -> @@ -326,6 +325,7 @@ fun RoomPrivacyOption( } } +// Move this composable to design module if we want to reuse it in other screens @Composable fun LabelledTextField( label: String, From 74be5b121eec1b7de8e3616edf3e027614f2494d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 14:53:54 +0200 Subject: [PATCH 106/119] plug back button --- .../features/createroom/impl/configureroom/ConfigureRoomNode.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt index 6e11e492b8..ec5bf67787 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt @@ -51,6 +51,7 @@ class ConfigureRoomNode @AssistedInject constructor( ConfigureRoomView( state = state, modifier = modifier, + onBackPressed = { navigateUp() } // TODO we should keep in memory the current view state ) } } From 7cc0137b0bbc2e8a4775e5309c3983ca11f2004b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 13 Apr 2023 15:47:23 +0200 Subject: [PATCH 107/119] Fix typo --- tools/adb/deeplink.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/adb/deeplink.sh b/tools/adb/deeplink.sh index a88d1083b3..5d50ec9409 100755 --- a/tools/adb/deeplink.sh +++ b/tools/adb/deeplink.sh @@ -18,7 +18,7 @@ # Format is: # elementx://open/{sessionId} to open a session # elementx://open/{sessionId}/{roomId} to open a room -# elementx://open/{sessionId}/{roomId}/{eventId} to open an event +# elementx://open/{sessionId}/{roomId}/{eventId} to open a thread # Open a session # adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org From 023c5f4a7edd8d7747436ca16a1af8b9d94af042 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 15:29:55 +0200 Subject: [PATCH 108/119] Use string resources --- .../impl/configureroom/ConfigureRoomView.kt | 23 +++++++++++-------- .../impl/root/CreateRoomRootView.kt | 2 +- .../src/main/res/values-es/translations.xml | 2 ++ .../src/main/res/values-it/translations.xml | 2 ++ .../src/main/res/values-ro/translations.xml | 2 ++ .../impl/src/main/res/values/localazy.xml | 2 ++ .../src/main/res/values-es/translations.xml | 4 ++++ .../src/main/res/values-it/translations.xml | 4 ++++ .../src/main/res/values-ro/translations.xml | 4 ++++ .../src/main/res/values-es/translations.xml | 4 +--- .../src/main/res/values-it/translations.xml | 4 +--- .../src/main/res/values-ro/translations.xml | 4 +--- .../src/main/res/values/localazy.xml | 3 +-- tools/localazy/config.json | 3 ++- 14 files changed, 40 insertions(+), 23 deletions(-) create mode 100644 libraries/androidutils/src/main/res/values-es/translations.xml create mode 100644 libraries/androidutils/src/main/res/values-it/translations.xml create mode 100644 libraries/androidutils/src/main/res/values-ro/translations.xml diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 6f56ae06ef..255e208532 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -52,6 +53,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import coil.request.ImageRequest +import io.element.android.features.createroom.impl.R import io.element.android.features.userlist.api.SelectedUsersList import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -64,6 +66,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.TextButton import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.ui.strings.R as StringR @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -126,7 +129,7 @@ fun ConfigureRoomToolbar( modifier = modifier, title = { Text( - text = "Create a room", + text = stringResource(R.string.screen_create_room_title), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, ) @@ -139,7 +142,7 @@ fun ConfigureRoomToolbar( onClick = onNextPressed, ) { Text( - text = "Create", + text = stringResource(StringR.string.action_create), fontSize = 16.sp, ) } @@ -166,9 +169,9 @@ fun RoomNameWithAvatar( ) LabelledTextField( - label = "Room name", + label = stringResource(R.string.screen_create_room_room_name_label), value = roomName, - placeholder = "e.g. Product Sprint", + placeholder = stringResource(R.string.screen_create_room_room_name_placeholder), onValueChange = onRoomNameChanged ) } @@ -217,9 +220,9 @@ fun RoomTopic( ) { LabelledTextField( modifier = modifier, - label = "Topic (optional)", + label = stringResource(R.string.screen_create_room_topic_label), value = topic, - placeholder = "What is this room about?", + placeholder = stringResource(R.string.screen_create_room_topic_placeholder), onValueChange = onTopicChanged, maxLines = 3, ) @@ -244,14 +247,14 @@ fun RoomPrivacyOptions( RoomPrivacy.Public -> RoomPrivacyItem( privacy = it, icon = Icons.Outlined.Lock, - title = "Private room (invite only)", - description = "Messages in this room are encrypted. Encryption can’t be disabled afterwards.", + title = stringResource(R.string.screen_create_room_private_option_title), + description = stringResource(R.string.screen_create_room_private_option_description), ) RoomPrivacy.Private -> RoomPrivacyItem( privacy = it, icon = Icons.Outlined.Public, - title = "Public room (anyone)", - description = "Messages are not encrypted and anyone can read them. You can enable encryption at a later date.", + title = stringResource(R.string.screen_create_room_public_option_title), + description = stringResource(R.string.screen_create_room_public_option_description), ) } } 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 4e12dcaab0..bcf58b7d2d 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 @@ -110,7 +110,7 @@ fun CreateRoomRootView( } is Async.Failure -> { RetryDialog( - content = stringResource(id = StringR.string.screen_start_chat_error_starting_chat), + content = stringResource(id = R.string.screen_start_chat_error_starting_chat), onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) }, onRetry = { state.userListState.selectedUsers.firstOrNull() diff --git a/features/createroom/impl/src/main/res/values-es/translations.xml b/features/createroom/impl/src/main/res/values-es/translations.xml index f6248df74e..bb3d6fa0b8 100644 --- a/features/createroom/impl/src/main/res/values-es/translations.xml +++ b/features/createroom/impl/src/main/res/values-es/translations.xml @@ -3,4 +3,6 @@ "Nueva sala" "Invitar gente" "Añadir personas" + "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." \ 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 index ea0c0b10e1..1d6ce99b5f 100644 --- a/features/createroom/impl/src/main/res/values-it/translations.xml +++ b/features/createroom/impl/src/main/res/values-it/translations.xml @@ -3,4 +3,6 @@ "Nuova stanza" "Invita persone" "Aggiungi persone" + "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." \ 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 index 98839a883e..af6e3db1fa 100644 --- a/features/createroom/impl/src/main/res/values-ro/translations.xml +++ b/features/createroom/impl/src/main/res/values-ro/translations.xml @@ -3,4 +3,6 @@ "Cameră nouă" "Invitați persoane" "Adaugați persoane" + "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ă." \ No newline at end of file diff --git a/features/createroom/impl/src/main/res/values/localazy.xml b/features/createroom/impl/src/main/res/values/localazy.xml index 48f055e082..177d588f7e 100644 --- a/features/createroom/impl/src/main/res/values/localazy.xml +++ b/features/createroom/impl/src/main/res/values/localazy.xml @@ -12,4 +12,6 @@ "Create a room" "Topic (optional)" "What is this room about?" + "An error occurred when trying to start a chat" + "We can’t validate this user’s Matrix ID. The invite might not be received." \ No newline at end of file diff --git a/libraries/androidutils/src/main/res/values-es/translations.xml b/libraries/androidutils/src/main/res/values-es/translations.xml new file mode 100644 index 0000000000..80b2b88347 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-es/translations.xml @@ -0,0 +1,4 @@ + + + "No se encontró ninguna aplicación compatible con esta acción." + \ No newline at end of file diff --git a/libraries/androidutils/src/main/res/values-it/translations.xml b/libraries/androidutils/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..03aaf3ffd1 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-it/translations.xml @@ -0,0 +1,4 @@ + + + "Non è stata trovata alcuna app compatibile per gestire questa azione." + \ No newline at end of file diff --git a/libraries/androidutils/src/main/res/values-ro/translations.xml b/libraries/androidutils/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..d2149227c5 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-ro/translations.xml @@ -0,0 +1,4 @@ + + + "Nu a fost găsită nicio aplicație capabilă să gestioneze această acțiune." + \ 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 index 58b25eaf3c..564ede34a8 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -137,11 +137,9 @@ "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-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index 2eb58f0d6e..a8ec05115a 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -137,11 +137,9 @@ "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 ba066efae9..091dd7f36d 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -139,11 +139,9 @@ "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 de11a74eac..379dd5508f 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -136,6 +136,7 @@ "This is the beginning of %1$s." "This is the beginning of this conversation." "New" + "No Invites" "%1$s invited you" "Block user" "Check if you want to hide all current and future messages from this user" @@ -145,8 +146,6 @@ "Unblock" "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" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 37f8c895f1..9cbd6cd620 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -28,7 +28,8 @@ { "name": ":features:createroom:impl", "includeRegex": [ - "screen_create_room_.*" + "screen_create_room_.*", + "screen_start_chat_.*" ] }, { From dde2aad600e2ba5fc9bcec65adc5b68d65d495bf Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 16:12:31 +0200 Subject: [PATCH 109/119] screenshots --- ...p_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ..._ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...aultGroup_RadioButtonDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ...ultGroup_RadioButtonLightPreview_0_null,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.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,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.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,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.theme.components_null_DefaultGroup_RadioButtonDarkPreview_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.theme.components_null_DefaultGroup_RadioButtonLightPreview_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.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,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.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..22e0b97b81 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2327de2be62afdacae32b297347fbcd23cd5e6986f513d018068aa981ecc3941 +size 89757 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,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.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..54b8ae3f9e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50129ad0c4d75ff5e7b8ffd43b0ffdc40d77a78f5699f9b7194a9c9ef026af69 +size 82189 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_RadioButtonDarkPreview_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_RadioButtonDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e2b920b8ed --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:247d303776a79f310144487784fb3841a430a6b0fa7fdee28485ed6157e42354 +size 7056 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_RadioButtonLightPreview_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_RadioButtonLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b2c1d53b70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d93540b38b57540142d2e57e4ce2caf7424fcd2aa3017ab16449d60c1e156797 +size 6730 From 25fe88c59d601586f2866928f0d8469956b4cd25 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 16:45:35 +0200 Subject: [PATCH 110/119] Add tests for ConfigureRoomPresenter --- features/createroom/impl/build.gradle.kts | 1 + .../ConfigureRoomPresenterTests.kt | 115 ++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 77a5ed26be..edb35428a0 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(libs.test.robolectric) testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.userlist.impl) testImplementation(projects.features.userlist.test) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt new file mode 100644 index 0000000000..0934909460 --- /dev/null +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -0,0 +1,115 @@ +/* + * 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.features.createroom.impl.configureroom + +import android.net.Uri +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.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.A_MESSAGE +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ConfigureRoomPresenterTests { + + private lateinit var presenter: ConfigureRoomPresenter + + @Before + fun setup() { + presenter = ConfigureRoomPresenter(ConfigureRoomPresenterArgs(emptyList())) + } + + @Test + fun `present - initial state`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.roomName).isEmpty() + assertThat(initialState.topic).isEmpty() + assertThat(initialState.privacy).isNull() + } + } + + @Test + fun `present - create room button is enabled only if the required fields are completed`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isCreateButtonEnabled).isFalse() + + // Room name not empty + initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) + var newState: ConfigureRoomState = awaitItem() + assertThat(newState.roomName).isEqualTo(A_ROOM_NAME) + assertThat(newState.isCreateButtonEnabled).isFalse() + + // Select privacy + initialState.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Private)) + newState = awaitItem() + assertThat(newState.privacy).isEqualTo(RoomPrivacy.Private) + assertThat(newState.isCreateButtonEnabled).isTrue() + + // Clear room name + initialState.eventSink(ConfigureRoomEvents.RoomNameChanged("")) + newState = awaitItem() + assertThat(newState.roomName).isEqualTo("") + assertThat(newState.isCreateButtonEnabled).isFalse() + } + } + + @Test + fun `present - state is updated when fields are changed`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // Room name + initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) + val stateAfterRoomNameChanged = awaitItem() + assertThat(stateAfterRoomNameChanged.roomName).isEqualTo(A_ROOM_NAME) + + // Room topic + stateAfterRoomNameChanged.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE)) + val stateAfterTopicChanged = awaitItem() + assertThat(stateAfterTopicChanged.topic).isEqualTo(A_MESSAGE) + + // Room avatar + val anUri = Uri.parse(AN_AVATAR_URL) + stateAfterTopicChanged.eventSink(ConfigureRoomEvents.AvatarUriChanged(anUri)) + val stateAfterAvatarUriChanged = awaitItem() + assertThat(stateAfterAvatarUriChanged.avatarUri).isEqualTo(anUri) + + // Room privacy + stateAfterAvatarUriChanged.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Public)) + val stateAfterPrivacyChanged = awaitItem() + assertThat(stateAfterPrivacyChanged.privacy).isEqualTo(RoomPrivacy.Public) + } + } +} + From c50e199be4ea1d060aaf06a217aba5504477df43 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 16:49:01 +0200 Subject: [PATCH 111/119] Changelog --- changelog.d/110.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/110.feature diff --git a/changelog.d/110.feature b/changelog.d/110.feature new file mode 100644 index 0000000000..d28935b65f --- /dev/null +++ b/changelog.d/110.feature @@ -0,0 +1 @@ +[Create and join rooms] Create a room screen (UI) From 2e013c15d8aa0887100d0f9eb9ac86ab49536afa Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 11 Apr 2023 14:31:05 +0200 Subject: [PATCH 112/119] reorder params --- .../createroom/impl/configureroom/ConfigureRoomView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 255e208532..39b47eb60e 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -152,9 +152,9 @@ fun ConfigureRoomToolbar( @Composable fun RoomNameWithAvatar( - modifier: Modifier = Modifier, avatarUri: Uri?, roomName: String, + modifier: Modifier = Modifier, onAvatarClick: () -> Unit = {}, onRoomNameChanged: (String) -> Unit = {}, ) { @@ -179,8 +179,8 @@ fun RoomNameWithAvatar( @Composable fun Avatar( - modifier: Modifier = Modifier, avatarUri: Uri?, + modifier: Modifier = Modifier, onClick: () -> Unit = {}, ) { val commonModifier = modifier From 2e34c8e8eb1439601c3547e03efadc7c7003455a Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 15:54:48 +0200 Subject: [PATCH 113/119] declare node callback in local variable --- .../features/createroom/impl/CreateRoomFlowNode.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 137017a2ec..a50c9ab1d0 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 @@ -68,7 +68,7 @@ class CreateRoomFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Root -> { - createNode(context = buildContext, plugins = listOf(object : CreateRoomRootNode.Callback { + val callback = object : CreateRoomRootNode.Callback { override fun onCreateNewRoom() { backstack.push(NavTarget.NewRoom) } @@ -76,14 +76,16 @@ class CreateRoomFlowNode @AssistedInject constructor( override fun onOpenRoom(roomId: RoomId) { plugins().forEach { it.onOpenRoom(roomId) } } - })) + } + createNode(context = buildContext, plugins = listOf(callback)) } NavTarget.NewRoom -> { - createNode(context = buildContext, plugins = listOf(object : AddPeopleNode.Callback { + val callback = object : AddPeopleNode.Callback { override fun onContinue(selectedUsers: List) { backstack.push(NavTarget.ConfigureRoom(selectedUsers)) } - })) + } + createNode(context = buildContext, plugins = listOf(callback)) } is NavTarget.ConfigureRoom -> { createNode(context = buildContext, plugins = listOf(ConfigureRoomNode.Inputs(navTarget.users))) From 5699dcf39e8b6d273806563400dc60905f9611e3 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 16:18:33 +0200 Subject: [PATCH 114/119] Fix modifier usage --- .../features/createroom/impl/configureroom/ConfigureRoomView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 39b47eb60e..1ff41bc8af 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -203,7 +203,7 @@ fun Avatar( Icon( imageVector = Icons.Outlined.AddAPhoto, contentDescription = "", - modifier = modifier + modifier = Modifier .align(Alignment.Center) .size(40.dp), tint = MaterialTheme.colorScheme.secondary, From 4337a95a39470a96060fb8d975a5b5472227af68 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 17:49:14 +0200 Subject: [PATCH 115/119] use derived state --- .../impl/configureroom/ConfigureRoomPresenter.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index c8bad29f93..572d5aa2fb 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -18,8 +18,10 @@ package io.element.android.features.createroom.impl.configureroom import android.net.Uri import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import dagger.assisted.Assisted @@ -43,9 +45,10 @@ class ConfigureRoomPresenter @AssistedInject constructor( var topic by rememberSaveable { mutableStateOf("") } var avatarUri by rememberSaveable { mutableStateOf(null) } var privacy by rememberSaveable { mutableStateOf(null) } - val isCreateButtonEnabled by rememberSaveable(roomName, privacy) { - val enabled = roomName.isNotEmpty() && privacy != null - mutableStateOf(enabled) + val isCreateButtonEnabled by remember { + derivedStateOf { + roomName.isNotEmpty() && privacy != null + } } fun handleEvents(event: ConfigureRoomEvents) { From 878b9ccf331edec448d352dae1e615926fa65459 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 17:56:18 +0200 Subject: [PATCH 116/119] Fix hardcoding privacy option --- .../createroom/impl/configureroom/ConfigureRoomView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 1ff41bc8af..e61dc1524c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -261,12 +261,12 @@ fun RoomPrivacyOptions( Column(modifier = modifier.selectableGroup()) { items.forEach { item -> RoomPrivacyOption( - privacy = RoomPrivacy.Private, + privacy = item.privacy, icon = item.icon, title = item.title, description = item.description, isSelected = selected == item.privacy, - onOptionSelected = { onOptionSelected(item.privacy) } + onOptionSelected = onOptionSelected, ) } } From 54b76078a8dc21a55b9f9a830a28dbd706780dc0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 18:06:17 +0200 Subject: [PATCH 117/119] Pass item to RoomPrivacyOption --- .../impl/configureroom/ConfigureRoomView.kt | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index e61dc1524c..ee718b555a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -111,7 +111,7 @@ fun ConfigureRoomView( RoomPrivacyOptions( modifier = Modifier.padding(bottom = 40.dp), selected = state.privacy, - onOptionSelected = { state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it)) }, + onOptionSelected = { state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it.privacy)) }, ) } } @@ -228,20 +228,19 @@ fun RoomTopic( ) } +data class RoomPrivacyItem( + val privacy: RoomPrivacy, + val icon: ImageVector, + val title: String, + val description: String, +) + @Composable fun RoomPrivacyOptions( selected: RoomPrivacy?, modifier: Modifier = Modifier, - onOptionSelected: (RoomPrivacy) -> Unit = {}, + onOptionSelected: (RoomPrivacyItem) -> Unit = {}, ) { - - data class RoomPrivacyItem( - val privacy: RoomPrivacy, - val icon: ImageVector, - val title: String, - val description: String, - ) - val items = RoomPrivacy.values().map { when (it) { RoomPrivacy.Public -> RoomPrivacyItem( @@ -261,10 +260,7 @@ fun RoomPrivacyOptions( Column(modifier = modifier.selectableGroup()) { items.forEach { item -> RoomPrivacyOption( - privacy = item.privacy, - icon = item.icon, - title = item.title, - description = item.description, + roomPrivacyItem = item, isSelected = selected == item.privacy, onOptionSelected = onOptionSelected, ) @@ -274,27 +270,24 @@ fun RoomPrivacyOptions( @Composable fun RoomPrivacyOption( - privacy: RoomPrivacy, - icon: ImageVector, - title: String, - description: String, + roomPrivacyItem: RoomPrivacyItem, modifier: Modifier = Modifier, isSelected: Boolean = false, - onOptionSelected: (RoomPrivacy) -> Unit = {}, + onOptionSelected: (RoomPrivacyItem) -> Unit = {}, ) { Row( modifier .fillMaxWidth() .selectable( selected = isSelected, - onClick = { onOptionSelected(privacy) }, + onClick = { onOptionSelected(roomPrivacyItem) }, role = Role.RadioButton, ) .padding(8.dp), ) { Icon( modifier = Modifier.padding(horizontal = 8.dp), - imageVector = icon, + imageVector = roomPrivacyItem.icon, contentDescription = "", tint = MaterialTheme.colorScheme.secondary, ) @@ -305,13 +298,13 @@ fun RoomPrivacyOption( .padding(horizontal = 8.dp) ) { Text( - text = title, + text = roomPrivacyItem.title, fontSize = 16.sp, color = MaterialTheme.colorScheme.primary, ) Spacer(Modifier.size(3.dp)) Text( - text = description, + text = roomPrivacyItem.description, fontSize = 12.sp, lineHeight = 17.sp, color = MaterialTheme.colorScheme.tertiary, From 97b5fa7ea8bd78882292215c59f262c5ac183fbe Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 13 Apr 2023 18:41:57 +0200 Subject: [PATCH 118/119] [Room Details] Implement member details screen (#302) --- changelog.d/300.feature | 1 + .../roomdetails/impl/RoomDetailsFlowNode.kt | 18 +- .../roomdetails/impl/RoomDetailsView.kt | 7 +- ...mberModule.kt => RoomMemberBindsModule.kt} | 22 ++- .../impl/members/RoomMemberListNode.kt | 18 +- .../details/RoomMemberDetailsEvents.kt | 22 +++ .../members/details/RoomMemberDetailsNode.kt | 80 ++++++++ .../details/RoomMemberDetailsPresenter.kt | 51 +++++ .../members/details/RoomMemberDetailsState.kt | 25 +++ .../details/RoomMemberDetailsStateProvider.kt | 37 ++++ .../members/details/RoomMemberDetailsView.kt | 177 ++++++++++++++++++ .../roomdetails/RoomDetailsPresenterTests.kt | 4 +- .../members/RoomMemberListPresenterTests.kt | 2 + .../RoomMemberDetailsPresenterTests.kt | 48 +++++ .../libraries/matrix/api/core/RoomId.kt | 6 +- .../libraries/matrix/api/core/UserId.kt | 6 +- .../matrix/api/permalink/PermalinkBuilder.kt | 31 ++- .../libraries/matrix/api/room/MatrixRoom.kt | 3 + .../libraries/matrix/api/room/RoomMember.kt | 12 +- .../matrix/impl/room/RoomMemberMapper.kt | 1 + .../matrix/impl/room/RustMatrixRoom.kt | 5 + .../matrix/test/room/FakeMatrixRoom.kt | 5 + .../src/main/res/values/localazy.xml | 45 +++++ ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + 29 files changed, 629 insertions(+), 15 deletions(-) create mode 100644 changelog.d/300.feature rename features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/{RoomMemberModule.kt => RoomMemberBindsModule.kt} (61%) create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt create mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_0,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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_0,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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png diff --git a/changelog.d/300.feature b/changelog.d/300.feature new file mode 100644 index 0000000000..fba672e002 --- /dev/null +++ b/changelog.d/300.feature @@ -0,0 +1 @@ +Implement room member details screen diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 4a46f3370c..5778eae96e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -29,10 +29,13 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.roomdetails.impl.members.RoomMemberListNode +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode 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.RoomScope +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.RoomMember import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) @@ -54,6 +57,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Parcelize object RoomMemberList : NavTarget + + @Parcelize + data class RoomMemberDetails(val roomMember: RoomMember) : NavTarget } interface Callback : Plugin { @@ -69,7 +75,17 @@ class RoomDetailsFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.RoomDetails -> createNode(buildContext, listOf(callback)) - NavTarget.RoomMemberList -> createNode(buildContext) + NavTarget.RoomMemberList -> { + val callback = object : RoomMemberListNode.Callback { + override fun openRoomMemberDetails(roomMember: RoomMember) { + backstack.push(NavTarget.RoomMemberDetails(roomMember)) + } + } + createNode(buildContext, listOf(callback)) + } + is NavTarget.RoomMemberDetails -> { + createNode(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMember))) + } } } 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 d5b2a4a9e4..a4e7c8f77e 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 @@ -87,7 +87,7 @@ fun RoomDetailsView( roomAlias = state.roomAlias ) - ShareSection(onShareRoom = onShareRoom) + ShareSection(onShareUser = onShareRoom) if (state.roomTopic != null) { TopicSection(roomTopic = state.roomTopic) @@ -127,12 +127,12 @@ fun RoomDetailsView( } @Composable -internal fun ShareSection(onShareRoom: () -> Unit, modifier: Modifier = Modifier) { +internal fun ShareSection(onShareUser: () -> Unit, modifier: Modifier = Modifier) { PreferenceCategory(modifier = modifier) { PreferenceText( title = stringResource(R.string.screen_room_details_share_room_title), icon = Icons.Outlined.Share, - onClick = onShareRoom, + onClick = onShareUser, ) } } @@ -172,7 +172,6 @@ internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) { color = MaterialTheme.colorScheme.tertiary ) } - } @Composable diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberBindsModule.kt similarity index 61% rename from features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt rename to features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberBindsModule.kt index 49c98374f2..e539e9070a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberBindsModule.kt @@ -19,17 +19,35 @@ package io.element.android.features.roomdetails.impl.di import com.squareup.anvil.annotations.ContributesTo import dagger.Binds import dagger.Module +import dagger.Provides import io.element.android.features.roomdetails.impl.members.RoomMatrixUserDataSource +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.userlist.api.MatrixUserDataSource import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMember import javax.inject.Named @Module @ContributesTo(RoomScope::class) -interface RoomMemberModule { +interface RoomMemberBindsModule { @Binds @Named("RoomMembers") fun bindRoomMemberUserListDataSource(dataSource: RoomMatrixUserDataSource): MatrixUserDataSource - +} + +@Module +@ContributesTo(RoomScope::class) +object RoomMemberProvidesModule { + @Provides + fun provideRoomMemberDetailsPresenterFactory( + room: MatrixRoom, + ): RoomMemberDetailsPresenter.Factory { + return object : RoomMemberDetailsPresenter.Factory { + override fun create(roomMember: RoomMember): RoomMemberDetailsPresenter { + return RoomMemberDetailsPresenter(room, roomMember) + } + } + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index 0f65b4657b..59f13115f0 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -21,10 +21,14 @@ 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 com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.roomdetails.impl.RoomDetailsFlowNode import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.ui.model.MatrixUser import timber.log.Timber @@ -32,11 +36,23 @@ import timber.log.Timber class RoomMemberListNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val room: MatrixRoom, private val presenter: RoomMemberListPresenter, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun openRoomMemberDetails(roomMember: RoomMember) + } + + private val callback = plugins().first() + private fun onUserSelected(matrixUser: MatrixUser) { - Timber.d("TODO: implement user selection. User: $matrixUser") + val member = room.getMember(matrixUser.id) + if (member != null) { + callback.openRoomMemberDetails(member) + } else { + Timber.e("Could find room member ${matrixUser.id} in room ${room.roomId}") + } } @Composable diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt new file mode 100644 index 0000000000..2ce6688925 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.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.features.roomdetails.impl.members.details + +// TODO Add your events or remove the file completely if no events +sealed interface RoomMemberDetailsEvents { + object MyEvent : RoomMemberDetailsEvents +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt new file mode 100644 index 0000000000..fe7f816a5a --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt @@ -0,0 +1,80 @@ +/* + * 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.roomdetails.impl.members.details + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +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.features.roomdetails.impl.R +import io.element.android.libraries.androidutils.system.startSharePlainTextIntent +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder +import io.element.android.libraries.matrix.api.room.RoomMember +import timber.log.Timber +import io.element.android.libraries.androidutils.R as AndroidUtilsR + +@ContributesNode(RoomScope::class) +class RoomMemberDetailsNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: RoomMemberDetailsPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val member: RoomMember, + ) : NodeInputs + + private val inputs = inputs() + private val presenter = presenterFactory.create(inputs.member) + + private fun onShareUser(context: Context) { + val permalinkResult = PermalinkBuilder.permalinkForUser(UserId(inputs.member.userId)) + permalinkResult.onSuccess { permalink -> + startSharePlainTextIntent( + context = context, + activityResultLauncher = null, + chooserTitle = context.getString(R.string.screen_room_details_share_room_title), + text = permalink, + noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found) + ) + }.onFailure { + Timber.e(it) + } + } + + @Composable + override fun View(modifier: Modifier) { + val context = LocalContext.current + val state = presenter.present() + RoomMemberDetailsView( + state = state, + modifier = modifier, + goBack = { navigateUp() }, + onShareUser = { onShareUser(context) } + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt new file mode 100644 index 0000000000..0e997c4499 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.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.features.roomdetails.impl.members.details + +import androidx.compose.runtime.Composable +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMember + +class RoomMemberDetailsPresenter @AssistedInject constructor( + private val room: MatrixRoom, + @Assisted private val roomMember: RoomMember, +) : Presenter { + + interface Factory { + fun create(roomMember: RoomMember): RoomMemberDetailsPresenter + } + + @Composable + override fun present(): RoomMemberDetailsState { + +// fun handleEvents(event: RoomMemberDetailsEvents) { +// when (event) { +// } +// } + + return RoomMemberDetailsState( + userId = roomMember.userId, + userName = roomMember.displayName, + avatarUrl = roomMember.avatarUrl, + isBlocked = roomMember.isIgnored, +// eventSink = ::handleEvents + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt new file mode 100644 index 0000000000..d9e3f949e7 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.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.roomdetails.impl.members.details + +data class RoomMemberDetailsState( + val userId: String, + val userName: String?, + val avatarUrl: String?, + val isBlocked: Boolean, +// val eventSink: (RoomMemberDetailsEvents) -> Unit +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt new file mode 100644 index 0000000000..c719ab7a26 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt @@ -0,0 +1,37 @@ +/* + * 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.roomdetails.impl.members.details + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class RoomMemberDetailsStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomMemberDetailsState(), + aRoomMemberDetailsState().copy(userName = null), + aRoomMemberDetailsState().copy(isBlocked = true), + // Add other states here + ) +} + +fun aRoomMemberDetailsState() = RoomMemberDetailsState( + userId = "@daniel:domain.com", + userName = "Daniel", + avatarUrl = null, + isBlocked = false, +// eventSink = {}, +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt new file mode 100644 index 0000000000..d7a97e5fdc --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt @@ -0,0 +1,177 @@ +/* + * 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.roomdetails.impl.members.details + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Block +import androidx.compose.material.icons.outlined.ChatBubbleOutline +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.roomdetails.impl.R +import io.element.android.libraries.designsystem.ElementTextStyles +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.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceText +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +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 +fun RoomMemberDetailsView( + state: RoomMemberDetailsState, + onShareUser: () -> Unit, + goBack: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar(title = { }, navigationIcon = { BackButton(onClick = goBack) }) + }, + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .verticalScroll(rememberScrollState()) + ) { + HeaderSection( + avatarUrl = state.avatarUrl, + userId = state.userId, + userName = state.userName, + ) + + ShareSection(onShareUser = onShareUser) + + SendMessageSection(onSendMessage = { + // TODO implement send DM + }) + + BlockSection(isBlocked = state.isBlocked, onToggleBlock = { + // TODO implement block & unblock + }) + } + } +} + +@Composable +internal fun HeaderSection( + avatarUrl: String?, + userId: String, + userName: String?, + modifier: Modifier = Modifier +) { + Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Box(modifier = Modifier.size(70.dp)) { + Avatar( + avatarData = AvatarData(userId, userName, avatarUrl, AvatarSize.HUGE), + modifier = Modifier.fillMaxSize() + ) + } + Spacer(modifier = Modifier.height(30.dp)) + if (userName != null) { + Text(userName, style = ElementTextStyles.Bold.title1) + Spacer(modifier = Modifier.height(8.dp)) + } + Text(userId, style = ElementTextStyles.Regular.body, color = MaterialTheme.colorScheme.secondary) + Spacer(Modifier.height(32.dp)) + } +} + +@Composable +internal fun ShareSection(onShareUser: () -> Unit, modifier: Modifier = Modifier) { + PreferenceCategory(modifier = modifier) { + PreferenceText( + title = stringResource(StringR.string.action_share), + icon = Icons.Outlined.Share, + onClick = onShareUser, + ) + } +} + +@Composable +internal fun SendMessageSection(onSendMessage: () -> Unit, modifier: Modifier = Modifier) { + PreferenceCategory(modifier = modifier) { + PreferenceText( + title = stringResource(StringR.string.action_send_message), + icon = Icons.Outlined.ChatBubbleOutline, + onClick = onSendMessage, + ) + } +} + +@Composable +internal fun BlockSection(isBlocked: Boolean, onToggleBlock: () -> Unit, modifier: Modifier = Modifier) { + PreferenceCategory(showDivider = false, modifier = modifier) { + if (isBlocked) { + PreferenceText( + title = stringResource(R.string.screen_dm_details_unblock_user), + icon = Icons.Outlined.Block, + ) + } else { + PreferenceText( + title = stringResource(R.string.screen_dm_details_block_user), + icon = Icons.Outlined.Block, + tintColor = LocalColors.current.textActionCritical, + ) + } + } +} + +@Preview +@Composable +fun RoomMemberDetailsViewLightPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun RoomMemberDetailsViewDarkPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: RoomMemberDetailsState) { + RoomMemberDetailsView( + state = state, + onShareUser = {}, + goBack = {}, + ) +} 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 41404b7354..6292f877d7 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 @@ -233,7 +233,8 @@ fun aRoomMember( membership: RoomMembershipState = RoomMembershipState.JOIN, isNameAmbiguous: Boolean = false, powerLevel: Long = 0L, - normalizedPowerLevel: Long = 0L + normalizedPowerLevel: Long = 0L, + isIgnored: Boolean = false, ) = RoomMember( userId = userId.value, displayName = displayName, @@ -242,4 +243,5 @@ fun aRoomMember( isNameAmbiguous = isNameAmbiguous, powerLevel = powerLevel, normalizedPowerLevel = normalizedPowerLevel, + isIgnored = isIgnored, ) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index 3564daa5f1..1263ecff5d 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -29,10 +29,12 @@ import io.element.android.features.userlist.impl.DefaultUserListPresenter import io.element.android.features.userlist.test.FakeMatrixUserDataSource import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.ui.components.aMatrixUser +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.internal.toImmutableList import org.junit.Test +@ExperimentalCoroutinesApi class RoomMemberListPresenterTests { @Test diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt new file mode 100644 index 0000000000..de4904fb7f --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.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.features.roomdetails.members.details + +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.aMatrixRoom +import io.element.android.features.roomdetails.aRoomMember +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@ExperimentalCoroutinesApi +class RoomMemberDetailsPresenterTests { + + @Test + fun `present - returns the room member's data`() = runTest { + val room = aMatrixRoom() + val roomMember = aRoomMember(displayName = "Alice") + val presenter = RoomMemberDetailsPresenter(room, roomMember) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.userId).isEqualTo(roomMember.userId) + Truth.assertThat(initialState.userName).isEqualTo(roomMember.displayName) + Truth.assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl) + Truth.assertThat(initialState.isBlocked).isEqualTo(roomMember.isIgnored) + } + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt index e31b8063df..9ff8c1d76d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt @@ -20,7 +20,11 @@ import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable @JvmInline -value class RoomId(val value: String) : Serializable +value class RoomId(val value: String) : Serializable { + override fun toString(): String { + return value + } +} fun String.asRoomId() = if (BuildConfig.DEBUG && !MatrixPatterns.isRoomId(this)) { error("`$this` is not a valid room Id") diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt index 46adcdd59c..93810f8815 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt @@ -20,7 +20,11 @@ import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable @JvmInline -value class UserId(val value: String) : Serializable +value class UserId(val value: String) : Serializable { + override fun toString(): String { + return value + } +} fun String.asUserId() = if (BuildConfig.DEBUG && !MatrixPatterns.isUserId(this)) { error("`$this` is not a valid user Id") diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt index 92e2bcef1d..2d8456be38 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt @@ -19,8 +19,14 @@ package io.element.android.libraries.matrix.api.permalink import io.element.android.libraries.matrix.api.config.MatrixConfiguration import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId object PermalinkBuilder { + + private const val ROOM_PATH = "room/" + private const val USER_PATH = "user/" + private const val GROUP_PATH = "group/" + private val permalinkBaseUrl get() = (MatrixConfiguration.clientPermalinkBaseUrl ?: MatrixConfiguration.matrixToPermalinkBaseUrl).also { var baseUrl = it if (!baseUrl.endsWith("/")) { @@ -31,6 +37,21 @@ object PermalinkBuilder { } } + fun permalinkForUser(userId: UserId): Result { + return if (MatrixPatterns.isUserId(userId.value)) { + val url = buildString { + append(permalinkBaseUrl) + if (!isMatrixTo()) { + append(USER_PATH) + } + append(userId.value) + } + Result.success(url) + } else { + Result.failure(PermalinkBuilderError.InvalidRoomAlias) + } + } + fun permalinkForRoomAlias(roomAlias: String): Result { return if (MatrixPatterns.isRoomAlias(roomAlias)) { Result.success(permalinkForRoomAliasOrId(roomAlias)) @@ -49,10 +70,18 @@ object PermalinkBuilder { private fun permalinkForRoomAliasOrId(value: String): String { val id = escapeId(value) - return permalinkBaseUrl + id + return buildString { + append(permalinkBaseUrl) + if (!isMatrixTo()) { + append(ROOM_PATH) + } + append(id) + } } private fun escapeId(value: String) = value.replace("/", "%2F") + + private fun isMatrixTo(): Boolean = permalinkBaseUrl.startsWith(MatrixConfiguration.matrixToPermalinkBaseUrl) } sealed class PermalinkBuilderError : Throwable() { 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 60ff09a15b..9a97fcc9cf 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 @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.api.room import io.element.android.libraries.matrix.api.core.EventId 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.timeline.MatrixTimeline import kotlinx.coroutines.flow.Flow import java.io.Closeable @@ -38,6 +39,8 @@ interface MatrixRoom: Closeable { suspend fun memberCount(): Int + fun getMember(userId: UserId): RoomMember? + fun syncUpdateFlow(): Flow fun timeline(): MatrixTimeline diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt index 66de9bd627..fcb83553c5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt @@ -16,6 +16,10 @@ package io.element.android.libraries.matrix.api.room +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize data class RoomMember( val userId: String, val displayName: String?, @@ -23,9 +27,11 @@ data class RoomMember( val membership: RoomMembershipState, val isNameAmbiguous: Boolean, val powerLevel: Long, - val normalizedPowerLevel: Long -) + val normalizedPowerLevel: Long, + val isIgnored: Boolean, +) : Parcelable -enum class RoomMembershipState { +@Parcelize +enum class RoomMembershipState : Parcelable { BAN, INVITE, JOIN, KNOCK, LEAVE } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomMemberMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomMemberMapper.kt index ebc93780ee..1fe0de6080 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomMemberMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomMemberMapper.kt @@ -32,6 +32,7 @@ object RoomMemberMapper { roomMember.isNameAmbiguous(), roomMember.powerLevel(), roomMember.normalizedPowerLevel(), + roomMember.isIgnored(), ) fun mapMembership(membershipState: RustMembershipState): RoomMembershipState = 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 fd5a63cb3b..67cddeee00 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 @@ -20,6 +20,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.matrix.api.core.EventId 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.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.timeline.MatrixTimeline @@ -68,6 +69,10 @@ class RustMatrixRoom( return members().size } + override fun getMember(userId: UserId): RoomMember? { + return cachedMembers.firstOrNull { it.userId == userId.value } + } + override fun syncUpdateFlow(): Flow { return slidingSyncUpdateFlow .filter { 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 1c6d580a3c..f213da2358 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 @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.test.room import io.element.android.libraries.matrix.api.core.EventId 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.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -85,6 +86,10 @@ class FakeMatrixRoom( } } + override fun getMember(userId: UserId): RoomMember? { + return members.firstOrNull { it.userId == userId.value } + } + override suspend fun sendMessage(message: String): Result { delay(100) return Result.success(Unit) diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index de11a74eac..fcade4a62b 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -121,15 +121,60 @@ "Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite." "Are you sure that you want to leave the room?" "%1$s Android" + "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$d member" "%1$d members" + + "%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" + "%1$d room change" "%1$d room changes" "Rageshake to report bug" + "Choose how to receive notifications" + "Background synchronization" + "Google Services" + "No valid Google Play Services found. Notifications may not work properly." "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?" "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages." "Reason for reporting this content" diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_0,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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9af1f155a8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6b4eaf1009581383dc6b95f1e27c3f4f94f63ef9e6b1875521ba5249f449ce9 +size 28705 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..75a3ccc4ee --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32b9585ac785804c27eba9a630d5634c79b08b276a11607ea80c83eacccb59a0 +size 26252 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..479406f955 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:312a9c09176df3772386c5bb6f1d6945ae2638101e790a991e6becaec2440b8a +size 29618 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_0,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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ea22da115e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf5bef51ccb02f801be411b46859f86d5ff0a571978763b589135eb12f1ec60e +size 28265 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a11a39bb0e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:625cad77761037dbf600a1ce7635257051d66138ffbde2663f94550a96d3007c +size 25872 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b982a61f9c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c210546e81ac92d6a3f2583dd3443e0314f2259aa455d1ecdf6da77048fd861 +size 28733 From d5e62dfbf17499a66f8b0f5d1557340f1231842d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 23:02:56 +0200 Subject: [PATCH 119/119] Split ConfigureRoomView into multiple files --- .../createroom/impl/components/Avatar.kt | 97 ++++++++++ .../impl/components/LabelledTextField.kt | 84 +++++++++ .../impl/components/RoomPrivacyOption.kt | 116 ++++++++++++ .../impl/configureroom/ConfigureRoomView.kt | 170 +----------------- .../impl/configureroom/RoomPrivacyItem.kt | 56 ++++++ ...atarDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...tarLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...ieldDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...eldLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...tionDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...ionLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + 11 files changed, 377 insertions(+), 164 deletions(-) create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_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.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_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.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_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.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_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.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionDarkPreview_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.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.kt new file mode 100644 index 0000000000..bbaf5c46e5 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.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.features.createroom.impl.components + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AddAPhoto +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground +import io.element.android.libraries.designsystem.theme.LocalColors +import io.element.android.libraries.designsystem.theme.components.Icon + +@Composable +fun Avatar( + avatarUri: Uri?, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + val commonModifier = modifier + .size(70.dp) + .clip(CircleShape) + .clickable(onClick = onClick) + + if (avatarUri != null) { + val context = LocalContext.current + val model = ImageRequest.Builder(context) + .data(avatarUri) + .build() + AsyncImage( + modifier = commonModifier, + model = model, + placeholder = debugPlaceholderBackground(ColorPainter(MaterialTheme.colorScheme.surfaceVariant)), + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } else { + Box(modifier = commonModifier.background(LocalColors.current.quinary)) { + Icon( + imageVector = Icons.Outlined.AddAPhoto, + contentDescription = "", + modifier = Modifier + .align(Alignment.Center) + .size(40.dp), + tint = MaterialTheme.colorScheme.secondary, + ) + } + } +} + +@Preview +@Composable +fun AvatarLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun AvatarDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Row { + Avatar(null) + Avatar(Uri.EMPTY) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt new file mode 100644 index 0000000000..382e4b8de2 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt @@ -0,0 +1,84 @@ +/* + * 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.createroom.impl.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.features.createroom.impl.R +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.designsystem.theme.components.TextField + +@Composable +fun LabelledTextField( + label: String, + value: String, + modifier: Modifier = Modifier, + placeholder: String = "", + maxLines: Int = 1, + onValueChange: (String) -> Unit = {}, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = label + ) + + TextField( + modifier = Modifier.fillMaxWidth(), + value = value, + placeholder = { Text(placeholder) }, + onValueChange = onValueChange, + maxLines = maxLines, + ) + } +} + +@Preview +@Composable +fun LabelledTextFieldLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun LabelledTextFieldDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + LabelledTextField( + label = stringResource(R.string.screen_create_room_room_name_label), + value = "", + placeholder = stringResource(R.string.screen_create_room_room_name_placeholder), + ) + LabelledTextField( + label = stringResource(R.string.screen_create_room_room_name_label), + value = "a room name", + placeholder = stringResource(R.string.screen_create_room_room_name_placeholder), + ) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt new file mode 100644 index 0000000000..8da6d43fcc --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt @@ -0,0 +1,116 @@ +/* + * 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.createroom.impl.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.selectable +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.features.createroom.impl.configureroom.RoomPrivacyItem +import io.element.android.features.createroom.impl.configureroom.roomPrivacyItems +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.RadioButton +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun RoomPrivacyOption( + roomPrivacyItem: RoomPrivacyItem, + modifier: Modifier = Modifier, + isSelected: Boolean = false, + onOptionSelected: (RoomPrivacyItem) -> Unit = {}, +) { + Row( + modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + onClick = { onOptionSelected(roomPrivacyItem) }, + role = Role.RadioButton, + ) + .padding(8.dp), + ) { + Icon( + modifier = Modifier.padding(horizontal = 8.dp), + imageVector = roomPrivacyItem.icon, + contentDescription = "", + tint = MaterialTheme.colorScheme.secondary, + ) + + Column( + Modifier + .weight(1f) + .padding(horizontal = 8.dp) + ) { + Text( + text = roomPrivacyItem.title, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(Modifier.size(3.dp)) + Text( + text = roomPrivacyItem.description, + fontSize = 12.sp, + lineHeight = 17.sp, + color = MaterialTheme.colorScheme.tertiary, + ) + } + + RadioButton( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(48.dp), + selected = isSelected, + onClick = null // null recommended for accessibility with screenreaders + ) + } +} + +@Preview +@Composable +fun RoomPrivacyOptionLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun RoomPrivacyOptionDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + val aRoomPrivacyItem = roomPrivacyItems().first() + Column { + RoomPrivacyOption( + roomPrivacyItem = aRoomPrivacyItem, + isSelected = true, + ) + RoomPrivacyOption( + roomPrivacyItem = aRoomPrivacyItem, + isSelected = false, + ) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index ee718b555a..ad0b9b0b79 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -14,61 +14,43 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3Api::class) + package io.element.android.features.createroom.impl.configureroom import android.net.Uri -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.AddAPhoto -import androidx.compose.material.icons.outlined.Lock -import androidx.compose.material.icons.outlined.Public import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil.compose.AsyncImage -import coil.request.ImageRequest import io.element.android.features.createroom.impl.R +import io.element.android.features.createroom.impl.components.Avatar +import io.element.android.features.createroom.impl.components.LabelledTextField +import io.element.android.features.createroom.impl.components.RoomPrivacyOption import io.element.android.features.userlist.api.SelectedUsersList import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.theme.LocalColors import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.RadioButton 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.TextButton -import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.ui.strings.R as StringR -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ConfigureRoomView( state: ConfigureRoomState, @@ -117,7 +99,6 @@ fun ConfigureRoomView( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ConfigureRoomToolbar( isNextActionEnabled: Boolean, @@ -177,41 +158,6 @@ fun RoomNameWithAvatar( } } -@Composable -fun Avatar( - avatarUri: Uri?, - modifier: Modifier = Modifier, - onClick: () -> Unit = {}, -) { - val commonModifier = modifier - .size(70.dp) - .clip(CircleShape) - .clickable(onClick = onClick) - - if (avatarUri != null) { - val context = LocalContext.current - val model = ImageRequest.Builder(context) - .data(avatarUri) - .build() - AsyncImage( - modifier = commonModifier, - model = model, - contentDescription = null, - ) - } else { - Box(modifier = commonModifier.background(LocalColors.current.quinary)) { - Icon( - imageVector = Icons.Outlined.AddAPhoto, - contentDescription = "", - modifier = Modifier - .align(Alignment.Center) - .size(40.dp), - tint = MaterialTheme.colorScheme.secondary, - ) - } - } -} - @Composable fun RoomTopic( topic: String, @@ -228,35 +174,13 @@ fun RoomTopic( ) } -data class RoomPrivacyItem( - val privacy: RoomPrivacy, - val icon: ImageVector, - val title: String, - val description: String, -) - @Composable fun RoomPrivacyOptions( selected: RoomPrivacy?, modifier: Modifier = Modifier, onOptionSelected: (RoomPrivacyItem) -> Unit = {}, ) { - val items = RoomPrivacy.values().map { - when (it) { - RoomPrivacy.Public -> RoomPrivacyItem( - privacy = it, - icon = Icons.Outlined.Lock, - title = stringResource(R.string.screen_create_room_private_option_title), - description = stringResource(R.string.screen_create_room_private_option_description), - ) - RoomPrivacy.Private -> RoomPrivacyItem( - privacy = it, - icon = Icons.Outlined.Public, - title = stringResource(R.string.screen_create_room_public_option_title), - description = stringResource(R.string.screen_create_room_public_option_description), - ) - } - } + val items = roomPrivacyItems() Column(modifier = modifier.selectableGroup()) { items.forEach { item -> RoomPrivacyOption( @@ -268,88 +192,6 @@ fun RoomPrivacyOptions( } } -@Composable -fun RoomPrivacyOption( - roomPrivacyItem: RoomPrivacyItem, - modifier: Modifier = Modifier, - isSelected: Boolean = false, - onOptionSelected: (RoomPrivacyItem) -> Unit = {}, -) { - Row( - modifier - .fillMaxWidth() - .selectable( - selected = isSelected, - onClick = { onOptionSelected(roomPrivacyItem) }, - role = Role.RadioButton, - ) - .padding(8.dp), - ) { - Icon( - modifier = Modifier.padding(horizontal = 8.dp), - imageVector = roomPrivacyItem.icon, - contentDescription = "", - tint = MaterialTheme.colorScheme.secondary, - ) - - Column( - Modifier - .weight(1f) - .padding(horizontal = 8.dp) - ) { - Text( - text = roomPrivacyItem.title, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.primary, - ) - Spacer(Modifier.size(3.dp)) - Text( - text = roomPrivacyItem.description, - fontSize = 12.sp, - lineHeight = 17.sp, - color = MaterialTheme.colorScheme.tertiary, - ) - } - - RadioButton( - modifier = Modifier - .align(Alignment.CenterVertically) - .size(48.dp), - selected = isSelected, - onClick = null // null recommended for accessibility with screenreaders - ) - } -} - -// Move this composable to design module if we want to reuse it in other screens -@Composable -fun LabelledTextField( - label: String, - value: String, - modifier: Modifier = Modifier, - placeholder: String = "", - maxLines: Int = 1, - onValueChange: (String) -> Unit, -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = label - ) - - TextField( - modifier = Modifier.fillMaxWidth(), - value = value, - placeholder = { Text(placeholder) }, - onValueChange = onValueChange, - maxLines = maxLines, - ) - } -} - @Preview @Composable fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt new file mode 100644 index 0000000000..0d1f6011c9 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.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.features.createroom.impl.configureroom + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.Public +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import io.element.android.features.createroom.impl.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +data class RoomPrivacyItem( + val privacy: RoomPrivacy, + val icon: ImageVector, + val title: String, + val description: String, +) + +@Composable +fun roomPrivacyItems(): ImmutableList { + return RoomPrivacy.values() + .map { + when (it) { + RoomPrivacy.Public -> RoomPrivacyItem( + privacy = it, + icon = Icons.Outlined.Lock, + title = stringResource(R.string.screen_create_room_private_option_title), + description = stringResource(R.string.screen_create_room_private_option_description), + ) + RoomPrivacy.Private -> RoomPrivacyItem( + privacy = it, + icon = Icons.Outlined.Public, + title = stringResource(R.string.screen_create_room_public_option_title), + description = stringResource(R.string.screen_create_room_public_option_description), + ) + } + } + .toImmutableList() +} diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_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.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e5f8881070 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aaeb60c55803711952413d99191209f467b4f77e1b03ad46601111691c2fc7fe +size 38188 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_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.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8197e52f75 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c8ccd79709924e19793977ebe5ab4f6ded7f20507d9534dc24f46513fdd7a69 +size 37844 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_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.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6f85c2f397 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b1243c92ee9f3735e81c2ab77910a7323b692a72c3c81c4b2ea776c1f0e5c84 +size 16267 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_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.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3229cb3561 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7db235009f0e0a50eda1196f2cd24eb490af10ebe42e26cc73fdf8ea2fdb0bf8 +size 15906 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionDarkPreview_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.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..582b3621ea --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5abeb4bb717e33b34ff0a61d657a01b4e7ad965b5d7f8e4252c7e956c4caada3 +size 38719 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionLightPreview_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.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..81a08165fa --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74c34325693f03c620a64493ea9bead27d8db1719d849ef4e1f589940efc73c9 +size 34559