Browse Source

Merge pull request #315 from vector-im/feature/bma/push2

Push - cleanup 1
test/jme/compound-poc
Benoit Marty 1 year ago committed by GitHub
parent
commit
018a5c540a
  1. 6
      app/build.gradle.kts
  2. 49
      app/src/debug/google-services.json
  3. 40
      app/src/nightly/google-services.json
  4. 40
      app/src/release/google-services.json
  5. 1
      appnav/build.gradle.kts
  6. 5
      appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt
  7. 2
      appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt
  8. 8
      appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt
  9. 1
      build.gradle.kts
  10. 2
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt
  11. 2
      gradle/libs.versions.toml
  12. 11
      libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt
  13. 4
      libraries/androidutils/src/main/res/values/localazy.xml
  14. 7
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt
  15. 24
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt
  16. 7
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt
  17. 8
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt
  18. 7
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt
  19. 7
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt
  20. 7
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt
  21. 1
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt
  22. 5
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt
  23. 6
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt
  24. 1
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt
  25. 3
      libraries/permissions/api/build.gradle.kts
  26. 10
      libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt
  27. 3
      libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt
  28. 10
      libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt
  29. 1
      libraries/push/api/build.gradle.kts
  30. 14
      libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt
  31. 48
      libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/model/BackgroundSyncMode.kt
  32. 18
      libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt
  33. 11
      libraries/push/impl/build.gradle.kts
  34. 50
      libraries/push/impl/src/main/AndroidManifest.xml
  35. 30
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt
  36. 49
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/FcmHelper.kt
  37. 104
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt
  38. 60
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt
  39. 179
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt
  40. 17
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/config/PushConfig.kt
  41. 11
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt
  42. 1
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt
  43. 32
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt
  44. 96
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt
  45. 28
      libraries/pushproviders/api/build.gradle.kts
  46. 22
      libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/Distributor.kt
  47. 12
      libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushData.kt
  48. 21
      libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushHandler.kt
  49. 51
      libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt
  50. 24
      libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt
  51. 7
      libraries/pushproviders/firebase/README.md
  52. 48
      libraries/pushproviders/firebase/build.gradle.kts
  53. 4
      libraries/pushproviders/firebase/src/debug/res/values/firebase.xml
  54. 31
      libraries/pushproviders/firebase/src/main/AndroidManifest.xml
  55. 20
      libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/EnsureFcmTokenIsRetrievedUseCase.kt
  56. 28
      libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt
  57. 57
      libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt
  58. 11
      libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParser.kt
  59. 58
      libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt
  60. 43
      libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseStore.kt
  61. 79
      libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseTroubleshooter.kt
  62. 17
      libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/PushDataFirebase.kt
  63. 19
      libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt
  64. 2
      libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingServiceBindings.kt
  65. 33
      libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/di/FirebaseModule.kt
  66. 10
      libraries/pushproviders/firebase/src/main/res/values/firebase.xml
  67. 4
      libraries/pushproviders/firebase/src/nightly/res/values/firebase.xml
  68. 4
      libraries/pushproviders/firebase/src/release/res/values/firebase.xml
  69. 90
      libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt
  70. 57
      libraries/pushproviders/unifiedpush/build.gradle.kts
  71. 47
      libraries/pushproviders/unifiedpush/src/main/AndroidManifest.xml
  72. 2
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/GuardServiceStarter.kt
  73. 4
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/KeepInternalDistributor.kt
  74. 31
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt
  75. 37
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt
  76. 28
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt
  77. 56
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushGatewayResolver.kt
  78. 52
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt
  79. 12
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt
  80. 63
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt
  81. 27
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt
  82. 25
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt
  83. 69
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt
  84. 2
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt
  85. 33
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/di/UnifiedPushModule.kt
  86. 25
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryResponse.kt
  87. 25
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryUnifiedPush.kt
  88. 24
      libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/UnifiedPushApi.kt
  89. 93
      libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt
  90. 27
      libraries/pushstore/api/build.gradle.kts
  91. 25
      libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt
  92. 26
      libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt
  93. 2
      libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecret.kt
  94. 2
      libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretFactory.kt
  95. 2
      libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretStore.kt
  96. 47
      libraries/pushstore/impl/build.gradle.kts
  97. 18
      libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt
  98. 33
      libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt
  99. 3
      libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretFactoryImpl.kt
  100. 5
      libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImpl.kt
  101. Some files were not shown because too many files have changed in this diff Show More

6
app/build.gradle.kts

@ -33,7 +33,8 @@ plugins { @@ -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 { @@ -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)

49
app/src/debug/google-services.json

@ -1,49 +0,0 @@ @@ -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"
}

40
app/src/nightly/google-services.json

@ -1,40 +0,0 @@ @@ -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"
}

40
app/src/release/google-services.json

@ -1,40 +0,0 @@ @@ -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"
}

1
appnav/build.gradle.kts

@ -45,6 +45,7 @@ dependencies { @@ -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)

5
appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt

@ -46,7 +46,10 @@ class LoggedInPresenter @Inject constructor( @@ -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()

2
appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt

@ -38,7 +38,7 @@ fun LoggedInView( @@ -38,7 +38,7 @@ fun LoggedInView(
state = state.permissionsState,
modifier = modifier,
openSystemSettings = {
activity?.let { openAppSettingsPage(it, "") }
activity?.let { openAppSettingsPage(it) }
}
)
}

8
appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt

@ -27,6 +27,8 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient @@ -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 { @@ -55,7 +57,11 @@ class LoggedInPresenterTest {
override fun notificationStyleChanged() {
}
override suspend fun registerFirebasePusher(matrixClient: MatrixClient) {
override fun getAvailablePushProviders(): List<PushProvider> {
return emptyList()
}
override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) {
}
override suspend fun testPush() {

1
build.gradle.kts

@ -230,7 +230,6 @@ koverMerged { @@ -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$*"
}

2
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 @@ -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( @@ -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)
)
}
}

2
gradle/libs.versions.toml

@ -133,6 +133,8 @@ sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", vers @@ -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"

11
libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt

@ -32,6 +32,7 @@ import androidx.activity.result.ActivityResultLauncher @@ -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 @@ -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 @@ -156,7 +157,7 @@ fun startNotificationChannelSettingsIntent(activity: Activity, channelID: String
fun startAddGoogleAccountIntent(
context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>,
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( @@ -171,7 +172,7 @@ fun startAddGoogleAccountIntent(
fun startInstallFromSourceIntent(
context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>,
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( @@ -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( @@ -217,7 +218,7 @@ fun startSharePlainTextIntent(
fun startImportTextFromFileIntent(
context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>,
noActivityFoundMessage: String,
noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found),
) {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
type = "text/plain"

4
libraries/androidutils/src/main/res/values/localazy.xml

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="error_no_compatible_app_found">"No compatible app was found to handle this action."</string>
</resources>

7
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt

@ -16,9 +16,14 @@ @@ -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)
}

24
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt

@ -91,6 +91,14 @@ object MatrixPatterns { @@ -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 { @@ -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 { @@ -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.
*

7
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt

@ -16,9 +16,14 @@ @@ -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)
}

8
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt

@ -16,6 +16,12 @@ @@ -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)
}

7
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt

@ -16,6 +16,7 @@ @@ -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 @@ -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)
}

7
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt

@ -16,9 +16,14 @@ @@ -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)
}

7
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt

@ -16,9 +16,14 @@ @@ -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)
}

1
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 @@ -18,4 +18,5 @@ package io.element.android.libraries.matrix.api.pusher
interface PushersService {
suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result<Unit>
suspend fun unsetHttpPusher(): Result<Unit>
}

5
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt

@ -53,4 +53,9 @@ class RustPushersService( @@ -53,4 +53,9 @@ class RustPushersService(
}
}
}
override suspend fun unsetHttpPusher(): Result<Unit> {
// 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)
}
}

6
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") @@ -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")

1
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 @@ -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<Unit> = Result.success(Unit)
}

3
libraries/permissions/api/build.gradle.kts

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.ksp)
}
android {
@ -27,4 +28,6 @@ dependencies { @@ -27,4 +28,6 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
ksp(libs.showkase.processor)
}

10
libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt

@ -32,9 +32,11 @@ fun PermissionsView( @@ -32,9 +32,11 @@ fun PermissionsView(
) {
if (state.showDialog.not()) return
if (state.permissionGranted) {
when {
state.permissionGranted -> {
// Notification Granted, nothing to do
} else if (state.permissionAlreadyDenied) {
}
state.permissionAlreadyDenied -> {
// In this case, tell the user to go to the settings
ConfirmationDialog(
modifier = modifier,
@ -47,7 +49,8 @@ fun PermissionsView( @@ -47,7 +49,8 @@ fun PermissionsView(
},
onDismiss = { state.eventSink.invoke(PermissionsEvents.CloseDialog) },
)
} else {
}
else -> {
val textToShow = if (state.shouldShowRationale) {
// TODO Move to state
// If the user has denied the permission but the rationale can be shown,
@ -75,6 +78,7 @@ fun PermissionsView( @@ -75,6 +78,7 @@ fun PermissionsView(
onDismiss = {}
)
}
}
}
@Preview

3
libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt

@ -23,7 +23,8 @@ open class PermissionsViewStateProvider : PreviewParameterProvider<PermissionsSt @@ -23,7 +23,8 @@ open class PermissionsViewStateProvider : PreviewParameterProvider<PermissionsSt
override val values: Sequence<PermissionsState>
get() = sequenceOf(
aPermissionsState(),
// Add other state here
aPermissionsState().copy(shouldShowRationale = true),
aPermissionsState().copy(permissionAlreadyDenied = true),
)
}

10
libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt

@ -33,6 +33,7 @@ import com.squareup.anvil.annotations.ContributesBinding @@ -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 @@ -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( @@ -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( @@ -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( @@ -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( @@ -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")
}
}

1
libraries/push/api/build.gradle.kts

@ -26,4 +26,5 @@ dependencies { @@ -26,4 +26,5 @@ dependencies {
implementation(libs.androidx.corektx)
implementation(libs.coroutines.core)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.pushproviders.api)
}

14
libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt

@ -17,12 +17,22 @@ @@ -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<PushProvider>
/**
* 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()
}

48
libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/model/BackgroundSyncMode.kt

@ -1,48 +0,0 @@ @@ -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
}
}

18
libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt

@ -16,26 +16,8 @@ @@ -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<Int>
// 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
}

11
libraries/push/impl/build.gradle.kts

@ -43,21 +43,20 @@ dependencies { @@ -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)

50
libraries/push/impl/src/main/AndroidManifest.xml

@ -14,63 +14,15 @@ @@ -14,63 +14,15 @@
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<!-- Firebase components -->
<meta-data
android:name="firebase_analytics_collection_deactivated"
android:value="true" />
<service
android:name=".firebase.VectorFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- UnifiedPush -->
<receiver
android:name=".unifiedpush.VectorUnifiedPushMessagingReceiver"
android:enabled="true"
android:exported="true"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE" />
<action android:name="org.unifiedpush.android.connector.UNREGISTERED" />
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT" />
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED" />
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED" />
</intent-filter>
</receiver>
<receiver
android:name=".unifiedpush.KeepInternalDistributor"
android:enabled="true"
android:exported="false">
<intent-filter>
<!--
This action is checked to track installed and uninstalled distributors.
We declare it to keep the background sync as an internal
unifiedpush distributor.
-->
<action android:name="org.unifiedpush.android.distributor.REGISTER" />
</intent-filter>
</receiver>
<receiver
android:name=".notifications.TestNotificationReceiver"
android:exported="false" />
<receiver
android:name=".notifications.NotificationBroadcastReceiver"
android:enabled="true"
android:exported="false" />
</application>
</manifest>

30
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt

@ -20,27 +20,41 @@ import com.squareup.anvil.annotations.ContributesBinding @@ -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<PushProvider> {
return pushProviders.sortedBy { it.index }
}
pushersManager.registerPusher(matrixClient, pushKey, PushConfig.pusher_http_url)
/**
* 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)
}
pushProvider.registerWith(matrixClient, distributor)
// Store new value
userPushStore.setPushProviderName(pushProvider.name)
}
override suspend fun testPush() {

49
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/FcmHelper.kt

@ -1,49 +0,0 @@ @@ -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)
*/
}

104
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt

@ -1,104 +0,0 @@ @@ -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"
}
}

60
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt

@ -16,20 +16,19 @@ @@ -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" @@ -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( @@ -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 {

179
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt

@ -1,179 +0,0 @@ @@ -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<DiscoveryResponse>(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<String> {
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()
}
}

17
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/config/PushConfig.kt

@ -17,25 +17,8 @@ @@ -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
}

11
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt

@ -70,7 +70,8 @@ class NotificationDrawerManager @Inject constructor( @@ -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( @@ -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( @@ -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()

1
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt

@ -34,6 +34,7 @@ data class NotificationEventQueue constructor( @@ -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<SessionId, EventId>.
private val seenEventIds: CircularCache<EventId>
) {

32
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt → libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt

@ -21,19 +21,22 @@ import android.content.Intent @@ -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 @@ -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( @@ -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( @@ -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( @@ -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")

96
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt

@ -17,20 +17,15 @@ @@ -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<Preferences> by preferencesDataStore(na @@ -42,7 +37,6 @@ private val Context.dataStore: DataStore<Preferences> 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( @@ -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"
}
}

28
libraries/pushproviders/api/build.gradle.kts

@ -0,0 +1,28 @@ @@ -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)
}

22
libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/Distributor.kt

@ -0,0 +1,22 @@ @@ -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,
)

12
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt → libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushData.kt

@ -14,7 +14,7 @@ @@ -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 @@ -22,14 +22,14 @@ import io.element.android.libraries.matrix.api.core.RoomId
/**
* Represent parsed data that the app has received from a Push content.
*
* @property eventId The Event ID. If not null, it will not be empty, and will have a valid format.
* @property roomId The Room ID. If not null, it will not be empty, and will have a valid format.
* @property eventId The Event Id.
* @property roomId The Room Id.
* @property unread Number of unread message.
* @property clientSecret A client secret, used to determine which user should receive the notification.
* @property clientSecret data used when the pusher was configured, to be able to determine the session.
*/
data class PushData(
val eventId: EventId?,
val roomId: RoomId?,
val eventId: EventId,
val roomId: RoomId,
val unread: Int?,
val clientSecret: String?,
)

21
libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushHandler.kt

@ -0,0 +1,21 @@ @@ -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)
}

51
libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt

@ -0,0 +1,51 @@ @@ -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<Distributor>
/**
* 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<Unit>
}

24
libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt

@ -0,0 +1,24 @@ @@ -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)
}

7
libraries/pushproviders/firebase/README.md

@ -0,0 +1,7 @@ @@ -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/<buildtype>/values/values.xml` to import the generated values into the `firebase.xml` files.

48
libraries/pushproviders/firebase/build.gradle.kts

@ -0,0 +1,48 @@ @@ -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)
}

4
libraries/pushproviders/firebase/src/debug/res/values/firebase.xml

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="google_app_id" translatable="false">1:912726360885:android:def0a4e454042e9b00427c</string>
</resources>

31
libraries/pushproviders/firebase/src/main/AndroidManifest.xml

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2023 New Vector Ltd
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<!-- Firebase components -->
<meta-data
android:name="firebase_analytics_collection_deactivated"
android:value="true" />
<service
android:name="io.element.android.libraries.push.providers.firebase.VectorFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>

20
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/EnsureFcmTokenIsRetrievedUseCase.kt → libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/EnsureFcmTokenIsRetrievedUseCase.kt

@ -14,24 +14,22 @@ @@ -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) {
/*

28
libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt

@ -0,0 +1,28 @@ @@ -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"
}

57
libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt

@ -0,0 +1,57 @@ @@ -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")
}
}
}
}

11
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt → libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParser.kt

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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<String, String?>): PushData {
fun parse(message: Map<String, String?>): 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()

58
libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt

@ -0,0 +1,58 @@ @@ -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<Distributor> {
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<Unit> {
return firebaseTroubleshooter.troubleshoot()
}
}

43
libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseStore.kt

@ -0,0 +1,43 @@ @@ -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"
}
}

79
libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseTroubleshooter.kt

@ -0,0 +1,79 @@ @@ -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<Unit> {
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
}
}

17
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt → libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/PushDataFirebase.kt

@ -14,12 +14,11 @@ @@ -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( @@ -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(),
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,
)
)
}

19
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt → libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt

@ -14,25 +14,23 @@ @@ -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() { @@ -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)
}
}
}

2
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingServiceBindings.kt → libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingServiceBindings.kt

@ -14,7 +14,7 @@ @@ -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

33
libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/di/FirebaseModule.kt

@ -0,0 +1,33 @@ @@ -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
}

10
libraries/pushproviders/firebase/src/main/res/values/firebase.xml

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="default_web_client_id" translatable="false">912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com</string>
<string name="firebase_database_url" translatable="false">https://vector-alpha.firebaseio.com</string>
<string name="gcm_defaultSenderId" translatable="false">912726360885</string>
<string name="google_api_key" translatable="false">AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c</string>
<string name="google_crash_reporting_api_key" translatable="false">AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c</string>
<string name="google_storage_bucket" translatable="false">vector-alpha.appspot.com</string>
<string name="project_id" translatable="false">vector-alpha</string>
</resources>

4
libraries/pushproviders/firebase/src/nightly/res/values/firebase.xml

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="google_app_id" translatable="false">1:912726360885:android:e17435e0beb0303000427c</string>
</resources>

4
libraries/pushproviders/firebase/src/release/res/values/firebase.xml

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="google_app_id" translatable="false">1:912726360885:android:d097de99a4c23d2700427c</string>
</resources>

90
libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt

@ -0,0 +1,90 @@ @@ -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<String, String?>.mutate(key: String, value: String?): Map<String, String?> {
return toMutableMap().apply { put(key, value) }
}

57
libraries/pushproviders/unifiedpush/build.gradle.kts

@ -0,0 +1,57 @@ @@ -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)
}

47
libraries/pushproviders/unifiedpush/src/main/AndroidManifest.xml

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2023 New Vector Ltd
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<receiver
android:name=".VectorUnifiedPushMessagingReceiver"
android:enabled="true"
android:exported="true"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE" />
<action android:name="org.unifiedpush.android.connector.UNREGISTERED" />
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT" />
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED" />
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED" />
</intent-filter>
</receiver>
<receiver
android:name=".KeepInternalDistributor"
android:enabled="true"
android:exported="false">
<intent-filter>
<!--
This action is checked to track installed and uninstalled distributors.
We declare it to keep the background sync as an internal
unifiedpush distributor.
-->
<action android:name="org.unifiedpush.android.distributor.REGISTER" />
</intent-filter>
</receiver>
</application>
</manifest>

2
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/GuardServiceStarter.kt → libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/GuardServiceStarter.kt

@ -14,7 +14,7 @@ @@ -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

4
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/KeepInternalDistributor.kt → libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/KeepInternalDistributor.kt

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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

31
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt → libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt

@ -14,12 +14,11 @@ @@ -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 @@ -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
)
}

37
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/RegisterUnifiedPushUseCase.kt → libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt

@ -14,55 +14,60 @@ @@ -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)
}
}

28
libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt

@ -0,0 +1,28 @@ @@ -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"
}

56
libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushGatewayResolver.kt

@ -0,0 +1,56 @@ @@ -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
}
}

52
libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt

@ -0,0 +1,52 @@ @@ -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")
}
}
}

12
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParser.kt → libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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<PushDataUnifiedPush>(String(message)) }?.toPushData()
private val json by lazy { Json { ignoreUnknownKeys = true } }
fun parse(message: ByteArray, clientSecret: String): PushData? {
return tryOrNull { json.decodeFromString<PushDataUnifiedPush>(String(message)) }?.toPushData(clientSecret)
}
}

63
libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt

@ -0,0 +1,63 @@ @@ -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<Distributor> {
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<Unit> {
TODO("Not yet implemented")
}
}

27
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.kt → libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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 @@ -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( @@ -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)
}
}

25
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnregisterUnifiedPushUseCase.kt → libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt

@ -14,39 +14,34 @@ @@ -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)
}
}

69
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt → libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt

@ -14,51 +14,39 @@ @@ -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<VectorUnifiedPushMessagingReceiverBindings>().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() { @@ -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 {
if (unifiedPushStore.getEndpoint(instance) != endpoint) {
unifiedPushStore.storeUpEndpoint(endpoint, instance)
coroutineScope.launch {
pushersManager.onNewUnifiedPushEndpoint(endpoint, it)
}
}
val gateway = unifiedPushGatewayResolver.getGateway(endpoint)
unifiedPushStore.storePushGateway(gateway, instance)
gateway?.let { pushGateway ->
newGatewayHandler.handle(endpoint, pushGateway, instance)
}
}
} else {
Timber.tag(loggerTag.value).i("onNewEndpoint: skipped")
}
}
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED
pushDataStore.setFdroidSyncBackgroundMode(mode)
guardServiceStarter.stop()
}
/**
* 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() { @@ -119,5 +117,6 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
Timber.tag(loggerTag.value).d("Probably unregistering a non existing pusher")
}
}
*/
}
}

2
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt → libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt

@ -14,7 +14,7 @@ @@ -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

33
libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/di/UnifiedPushModule.kt

@ -0,0 +1,33 @@ @@ -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
}

25
libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryResponse.kt

@ -0,0 +1,25 @@ @@ -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()
)

25
libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryUnifiedPush.kt

@ -0,0 +1,25 @@ @@ -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 = ""
)

24
libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/UnifiedPushApi.kt

@ -0,0 +1,24 @@ @@ -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
}

93
libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt

@ -0,0 +1,93 @@ @@ -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()
}

27
libraries/pushstore/api/build.gradle.kts

@ -0,0 +1,27 @@ @@ -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)
}

25
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt → libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt

@ -14,27 +14,24 @@ @@ -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

26
libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt

@ -0,0 +1,26 @@ @@ -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
}

2
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecret.kt → libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecret.kt

@ -14,7 +14,7 @@ @@ -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

2
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactory.kt → libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretFactory.kt

@ -14,7 +14,7 @@ @@ -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

2
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStore.kt → libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretStore.kt

@ -14,7 +14,7 @@ @@ -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

47
libraries/pushstore/impl/build.gradle.kts

@ -0,0 +1,47 @@ @@ -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)
}

18
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt → libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt

@ -14,28 +14,34 @@ @@ -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<String, UserPushStore>()
fun create(userId: String): UserPushStore {
private val cache = mutableMapOf<SessionId, UserPushStore>()
override fun create(userId: SessionId): UserPushStore {
return cache.getOrPut(userId) {
UserPushStoreDataStore(
context = context,
@ -54,6 +60,6 @@ class UserPushStoreFactory @Inject constructor( @@ -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() }
}
}

33
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreDataStore.kt → libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt

@ -14,14 +14,18 @@ @@ -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 @@ -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<Preferences> 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( @@ -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()

3
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt → libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretFactoryImpl.kt

@ -14,10 +14,11 @@ @@ -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

5
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImpl.kt → libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImpl.kt

@ -14,11 +14,14 @@ @@ -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)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save