diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0b8833efbb..772609f482 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,7 +33,8 @@ 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") + // To be able to update the firebase.xml files, uncomment and build the project + // id("com.google.gms.google-services") } android { @@ -225,9 +226,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) 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/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..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 @@ -46,7 +46,10 @@ class LoggedInPresenter @Inject constructor( override fun present(): LoggedInState { LaunchedEffect(Unit) { // Ensure pusher is registered - pushService.registerFirebasePusher(matrixClient) + // TODO Manually select push provider for now + val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect + val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect + pushService.registerWith(matrixClient, pushProvider, distributor) } 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 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/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..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,8 @@ 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 import org.junit.Test @@ -55,7 +57,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, distributor: Distributor) { } override suspend fun testPush() { 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$*" } 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2f39ffda8b..3176079351 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/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/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..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 @@ -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() = if (BuildConfig.DEBUG && !MatrixPatterns.isEventId(this)) { + error("`$this` is not a valid event Id") +} else { + EventId(this) +} 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..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 @@ -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() = if (BuildConfig.DEBUG && !MatrixPatterns.isRoomId(this)) { + error("`$this` is not a valid room Id") +} else { + 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 bea1f3c671..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 @@ -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() = if (BuildConfig.DEBUG && !MatrixPatterns.isSessionId(this)) { + error("`$this` is not a valid session Id") +} else { + 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 849dd7d637..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 @@ -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() = if (BuildConfig.DEBUG && !MatrixPatterns.isSpaceId(this)) { + error("`$this` is not a valid space Id") +} else { + 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 57fc187406..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 @@ -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() = if (BuildConfig.DEBUG && !MatrixPatterns.isThreadId(this)) { + error("`$this` is not a valid thread Id") +} else { + 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 216faade45..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 @@ -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() = if (BuildConfig.DEBUG && !MatrixPatterns.isUserId(this)) { + error("`$this` is not a valid user Id") +} else { + UserId(this) +} 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/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/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/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/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 = {} - ) } } 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), ) } 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..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: reset the store") + 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") } } 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..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,12 +17,22 @@ 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 { + // 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, distributor: Distributor) + + // TODO Move away suspend fun testPush() } 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/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/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 0968ac4344..81ba07fc63 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -43,21 +43,20 @@ 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) 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") } - implementation(platform(libs.google.firebase.bom)) - implementation("com.google.firebase:firebase-messaging-ktx") - - // UnifiedPush - api("com.github.UnifiedPush:android-connector:2.1.1") + // 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/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..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 @@ -20,27 +20,41 @@ 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.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager -import timber.log.Timber +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 fcmHelper: FcmHelper, + private val userPushStoreFactory: UserPushStoreFactory, + 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.") + override fun getAvailablePushProviders(): List { + return pushProviders.sortedBy { it.index } + } + + /** + * 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) + val currentPushProviderName = userPushStore.getPushProviderName() + if (currentPushProviderName != pushProvider.name) { + // Unregister previous one if any + pushProviders.find { it.name == currentPushProviderName }?.unregister(matrixClient) } - pushersManager.registerPusher(matrixClient, pushKey, PushConfig.pusher_http_url) + pushProvider.registerWith(matrixClient, distributor) + // 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/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/GoogleFcmHelper.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt deleted file mode 100755 index 6c73607196..0000000000 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt +++ /dev/null @@ -1,104 +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.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 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) -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) { - 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/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..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 @@ -16,20 +16,19 @@ 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 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.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.sessionstorage.api.SessionStore -import io.element.android.libraries.sessionstorage.api.toUserList +import io.element.android.libraries.push.providers.api.PusherSubscriber +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +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 @@ -38,62 +37,32 @@ 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 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, - 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 ) ) } - suspend fun enqueueRegisterPusherWithFcmKey(pushKey: String) { - // return onNewFirebaseToken(pushKey, PushConfig.pusher_http_url) - TODO() - } - - 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().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) { - val userDataStore = userPushStoreFactory.create(matrixClient.sessionId.value) + override suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) { + val userDataStore = userPushStoreFactory.create(matrixClient.sessionId) if (userDataStore.getCurrentRegisteredPushKey() == pushKey) { Timber.tag(loggerTag.value).d("Unnecessary to register again the same pusher") } else { @@ -162,9 +131,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/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 deleted file mode 100644 index 12ed3f1993..0000000000 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt +++ /dev/null @@ -1,179 +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 - -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 -import org.unifiedpush.android.connector.UnifiedPush -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 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()) { - 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 - 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(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() - } - - 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/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/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/PushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt similarity index 83% 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..09afe0a861 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,19 +21,22 @@ import android.content.Intent import android.os.Handler import android.os.Looper import androidx.localbroadcastmanager.content.LocalBroadcastManager -import io.element.android.libraries.androidutils.network.WifiDetector +import com.squareup.anvil.annotations.ContributesBinding 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.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.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 @@ -43,20 +46,20 @@ 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, 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 { private val coroutineScope = CoroutineScope(SupervisorJob()) - private val wifiDetector: WifiDetector = WifiDetector(context) // UI handler private val mUIHandler by lazy { @@ -68,7 +71,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) { @@ -84,12 +87,6 @@ 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 - } - mUIHandler.post { coroutineScope.launch(Dispatchers.IO) { handleInternal(pushData) } } @@ -134,6 +131,13 @@ class PushHandler @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/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/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/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 64% 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 864155e522..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 @@ -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 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..4ad0179403 --- /dev/null +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.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.api + +import io.element.android.libraries.matrix.api.MatrixClient + +/** + * This is the main API for this module. + */ +interface PushProvider { + /** + * Allow to sort providers, from lower index to higher index. + */ + val index: Int + + /** + * User friendly name. + */ + val name: String + + fun getDistributors(): List + + /** + * Register the pusher to the homeserver. + */ + suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) + + /** + * Unregister the pusher. + */ + suspend fun unregister(matrixClient: MatrixClient) + + /** + * Attempt to troubleshoot the push provider. + */ + suspend fun troubleshoot(): Result +} 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..0bf0f949f3 --- /dev/null +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.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.api + +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/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/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts new file mode 100644 index 0000000000..17f2071624 --- /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) +} + +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) + + api(platform(libs.google.firebase.bom)) + api("com.google.firebase:firebase-messaging-ktx") + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) +} 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/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..bf35a1b18a --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.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.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 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 new file mode 100644 index 0000000000..58464b5af0 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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 +import io.element.android.libraries.sessionstorage.api.toUserList +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("FirebaseNewTokenHandler") + +/** + * 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 handle(firebaseToken: String) { + firebaseStore.storeFcmToken(firebaseToken) + // Register the pusher for all the sessions + sessionStore.getAllSessions().toUserList() + .mapNotNull { 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") + } + } + } +} 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 71% 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 906816eb56..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,18 +14,17 @@ * 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.core.data.tryOrNull -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() { - fun parse(message: Map): PushData { + fun parse(message: Map): PushData? { 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() 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..15530033d5 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.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.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 +import javax.inject.Inject + +private val loggerTag = LoggerTag("FirebasePushProvider") + +class FirebasePushProvider @Inject constructor( + private val firebaseStore: FirebaseStore, + private val firebaseTroubleshooter: FirebaseTroubleshooter, + private val pusherSubscriber: PusherSubscriber, +) : PushProvider { + override val index = FirebaseConfig.index + override val name = FirebaseConfig.name + + override fun getDistributors(): List { + return listOf(Distributor("Firebase", "Firebase")) + } + + 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.") + } + 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/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/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 71% 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 bcf48bab15..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,12 +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.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 +import io.element.android.libraries.push.providers.api.PushData /** * In this case, the format is: @@ -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/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt similarity index 76% 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 8769baa947..35434ceb2e 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,25 +14,23 @@ * 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 firebaseNewTokenHandler: FirebaseNewTokenHandler @Inject lateinit var pushParser: FirebasePushParser @Inject lateinit var pushHandler: PushHandler @@ -46,15 +44,18 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { override fun onNewToken(token: String) { Timber.tag(loggerTag.value).d("New Firebase token") coroutineScope.launch { - pushersManager.onNewFirebaseToken(token) + firebaseNewTokenHandler.handle(token) } } 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/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/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 + 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 new file mode 100644 index 0000000000..562aecc790 --- /dev/null +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 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 io.element.android.tests.testutils.assertNullOrThrow +import org.junit.Test + +class FirebasePushParserTest { + private val validData = PushData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + unread = 1, + clientSecret = "a-secret" + ) + + @Test + fun `test edge cases Firebase`() { + val pushParser = FirebasePushParser() + // Empty Json + assertThat(pushParser.parse(emptyMap())).isNull() + // 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() + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", null))).isNull() + assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "")) } + } + + @Test + fun `test invalid roomId`() { + val pushParser = FirebasePushParser() + 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() + assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "")) } + } + + @Test + fun `test invalid eventId`() { + val pushParser = FirebasePushParser() + assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId")) } + } + + 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/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts new file mode 100644 index 0000000000..3546bb16e1 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.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 + api(libs.unifiedpush) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) +} 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 63% 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 56513ab970..618a3c989f 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,12 +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.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 +import io.element.android.libraries.push.providers.api.PushData import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -41,24 +40,28 @@ 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( - 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(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 = clientSecret + ) +} 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 53% 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..bff6b06876 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,55 +14,60 @@ * 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 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) - return RegisterUnifiedPushResult.Success - } - - if (!PushConfig.allowExternalUnifiedPushDistributors) { - saveAndRegisterApp(context.packageName) + suspend fun execute(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String): RegisterUnifiedPushResult { + val distributorValue = distributor.value + if (distributorValue.isNotEmpty()) { + saveAndRegisterApp(distributorValue, clientSecret) + val endpoint = unifiedPushStore.getEndpoint(clientSecret) ?: return RegisterUnifiedPushResult.Error + val gateway = unifiedPushStore.getPushGateway(clientSecret) ?: 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 new file mode 100644 index 0000000000..21b4ca9a76 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.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.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 index = 1 + const val name = "UnifiedPush" +} 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/UnifiedPushNewGatewayHandler.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt new file mode 100644 index 0000000000..3c9833010e --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.push.providers.api.PusherSubscriber +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret +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 userPushStoreFactory: UserPushStoreFactory, + private val pushClientSecret: PushClientSecret, + private val matrixAuthenticationService: MatrixAuthenticationService, +) { + 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/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 65% 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 9788ecf1a1..6169e1f8eb 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,16 +14,18 @@ * 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 class UnifiedPushParser @Inject constructor() { - fun parse(message: ByteArray): PushData? { - return tryOrNull { Json.decodeFromString(String(message)) }?.toPushData() + private val json by lazy { Json { ignoreUnknownKeys = true } } + + 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 new file mode 100644 index 0000000000..854c070d7e --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.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.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 io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret +import org.unifiedpush.android.connector.UnifiedPush +import javax.inject.Inject + +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 + + 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, distributor: Distributor) { + val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId) + registerUnifiedPushUseCase.execute(matrixClient, distributor, clientSecret) + } + + override suspend fun unregister(matrixClient: MatrixClient) { + val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId) + unRegisterUnifiedPushUseCase.execute(clientSecret) + } + + override suspend fun troubleshoot(): Result { + 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 65% 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..3883c3348c 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 @@ -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, @@ -33,40 +30,44 @@ 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(): String? { - return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN, null) + fun getEndpoint(clientSecret: String): String? { + return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, null) } /** * 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?) { + fun storeUpEndpoint(endpoint: String?, clientSecret: String) { defaultPrefs.edit { - putString(PREFS_ENDPOINT_OR_TOKEN, endpoint) + putString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, endpoint) } } /** * Retrieves the Push Gateway. * + * @param clientSecret the client secret, to identify the session * @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) } /** * 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?) { + fun storePushGateway(gateway: String?, clientSecret: String) { defaultPrefs.edit { - putString(PREFS_PUSH_GATEWAY, gateway) + putString(PREFS_PUSH_GATEWAY + clientSecret, gateway) } } 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 57% 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..e6eb778f7f 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,39 +14,34 @@ * 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.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 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, + private val unifiedPushGatewayResolver: UnifiedPushGatewayResolver, ) { - suspend fun execute(pushersManager: PushersManager?) { - val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME - pushDataStore.setFdroidSyncBackgroundMode(mode) + 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") - pushersManager?.unregisterPusher(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/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 60% 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 81dd389e78..0f065acc52 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,51 +14,39 @@ * 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 pushHandler: PushHandler @Inject lateinit var guardServiceStarter: GuardServiceStarter @Inject lateinit var unifiedPushStore: UnifiedPushStore - @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + @Inject lateinit var unifiedPushGatewayResolver: UnifiedPushGatewayResolver + @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 @@ -67,48 +55,58 @@ 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, instance) + if (pushData == null) { + Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush") + } else { + pushHandler.handle(pushData) } } } + /** + * 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. + */ 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 { - coroutineScope.launch { - pushersManager.onNewUnifiedPushEndpoint(endpoint, it) - } - } - } + // If the endpoint has changed + // or the gateway has changed + if (unifiedPushStore.getEndpoint(instance) != endpoint) { + unifiedPushStore.storeUpEndpoint(endpoint, instance) + coroutineScope.launch { + 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") } + } else { + Timber.tag(loggerTag.value).i("onNewEndpoint: skipped") } - 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) { + 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 pushDataStore.setFdroidSyncBackgroundMode(mode) guardServiceStarter.start() + */ } + /** + * 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() + /* val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME pushDataStore.setFdroidSyncBackgroundMode(mode) guardServiceStarter.start() @@ -119,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/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 new file mode 100644 index 0000000000..19231505cc --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 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 io.element.android.tests.testutils.assertNullOrThrow +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, + clientSecret = aClientSecret + ) + + @Test + fun `test edge cases UnifiedPush`() { + val pushParser = UnifiedPushParser() + // Empty string + assertThat(pushParser.parse("".toByteArray(), aClientSecret)).isNull() + // Empty Json + assertThat(pushParser.parse("{}".toByteArray(), aClientSecret)).isNull() + // Bad Json + assertThat(pushParser.parse("ABC".toByteArray(), aClientSecret)).isNull() + } + + @Test + fun `test UnifiedPush format`() { + val pushParser = UnifiedPushParser() + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.toByteArray(), aClientSecret)).isEqualTo(validData) + } + + @Test + fun `test empty roomId`() { + val pushParser = UnifiedPushParser() + assertNullOrThrow { + pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray(), aClientSecret) + } + } + + @Test + fun `test invalid roomId`() { + val pushParser = UnifiedPushParser() + assertNullOrThrow { + pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"), aClientSecret) + } + } + + @Test + fun `test empty eventId`() { + val pushParser = UnifiedPushParser() + assertNullOrThrow { + pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""), aClientSecret) + } + } + + @Test + fun `test invalid eventId`() { + val pushParser = UnifiedPushParser() + assertNullOrThrow { + pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"), aClientSecret) + } + } + + 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() +} diff --git a/libraries/pushstore/api/build.gradle.kts b/libraries/pushstore/api/build.gradle.kts new file mode 100644 index 0000000000..9a97bf693f --- /dev/null +++ b/libraries/pushstore/api/build.gradle.kts @@ -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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.pushstore.api" +} + +dependencies { + implementation(projects.libraries.matrix.api) +} 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 62% 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..28577ba3f8 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,27 +14,24 @@ * 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. */ interface UserPushStore { - /** - * [NOTIFICATION_METHOD_FIREBASE] or [NOTIFICATION_METHOD_UNIFIEDPUSH]. - */ - suspend fun getNotificationMethod(): String - - suspend fun setNotificationMethod(value: String) - + 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() } - -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..52e4596ca0 --- /dev/null +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.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.pushstore.api + +import io.element.android.libraries.matrix.api.core.SessionId + +/** + * Store data related to push about a user. + */ +interface UserPushStoreFactory { + 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 new file mode 100644 index 0000000000..4625f293cb --- /dev/null +++ b/libraries/pushstore/impl/build.gradle.kts @@ -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. + */ + +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.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/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 69% 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..ed32dba472 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,34 @@ * 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.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 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 { + private val cache = mutableMapOf() + override fun create(userId: SessionId): UserPushStore { return cache.getOrPut(userId) { UserPushStoreDataStore( context = context, @@ -54,6 +60,6 @@ class UserPushStoreFactory @Inject constructor( override suspend fun onSessionDeleted(userId: String) { // Delete the store - create(userId).reset() + userId.asSessionId()?.let { create(it).reset() } } } 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 60% 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..56867a6584 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,14 +14,18 @@ * 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 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 /** @@ -29,19 +33,20 @@ 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 notificationMethod = stringPreferencesKey("notificationMethod") + private val pushProviderName = stringPreferencesKey("pushProviderName") private val currentPushKey = stringPreferencesKey("currentPushKey") + private val notificationEnabled = booleanPreferencesKey("notificationEnabled") - override suspend fun getNotificationMethod(): String { - return context.dataStore.data.first()[notificationMethod] ?: NOTIFICATION_METHOD_FIREBASE + 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 } } @@ -55,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() 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 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/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 9f0cf92099..1427269755 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")) 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() + } +} 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 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": [