Skip to content

Update UnifiedPush library #4358

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4"
sqlite = "androidx.sqlite:sqlite-ktx:2.5.0"
unifiedpush = "com.github.UnifiedPush:android-connector:2.4.0"
unifiedpush = "org.unifiedpush.android:connector:3.0.8"
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.2"
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
Expand Down
8 changes: 7 additions & 1 deletion libraries/pushproviders/unifiedpush/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ dependencies {
implementation(libs.serialization.json)

// UnifiedPush library
api(libs.unifiedpush)
implementation(libs.unifiedpush) {
// Exclude package com.google.crypto.tink
// Duplicate classes between
// tink-1.16.0.jar -> tink-1.16.0 (com.google.crypto.tink:tink:1.16.0)
// tink-android-1.17.0.jar tink-android-1.8.0 (com.google.crypto.tink:tink-android:1.17.0)
exclude(group = "com.google.crypto.tink", module = "tink")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @p1gp1g , do you know why I need to add this line?

It seems to be related to https://github.com/UnifiedPush/android-connector/blob/cf347ae931ea8b6e6c773cd566abea940d1297f1/connector/build.gradle#L58 and the related PR tink-crypto/tink-java-apps#5, but maybe I miss something?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since version 3.0.0, the lib is able to decrypt webpush messages:

  • When a new endpoint comes, you receive a PushEndpoint that contains p256dh and auth secrets that can be send to the server, so the server can encrypt your push notifications following RFC8291.
  • When a new message comes, you receive a PushMessage, if the library has correctly decrypted the push message with the webpush keys, content is the cleartext and decrypted is true.

At this moment, matrix doesn't officially support webpush messages. So for the moment, decrypted will always be false. There is a MSC to add webpush pusher. As mentioned in the MSC, some web clients have support for webpush with a gateway by hacking around the specifications. Once this MSC supported, you can even push to FCM servers without any gateway, because FCM servers can be requested with webpush requests (so Android won't need sygnal with servers supporting this MSC, this will probably help in some cases).

The library uses tink library to decrypt the push messages, and I think you have another library using it too. So you end up with duplicate classes. Maybe you should explicitly add tink as a dependency and exclude it from other library using it as well

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fast and complete response. Maybe the unified push library should use tink-android and not tink and that would fix the conflicts? (even if I agree that we have another dependency (not checked which one yet) which has a dependency on an outdated version 1.8.0).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You would have the same issue if 2 dependencies rely on tink-android, isn't it ?

I have opened an issue to migrate to tink-android, there are some pro/con to do the migration there

Copy link
Member

@jmartinesp jmartinesp Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(even if I agree that we have another dependency (not checked which one yet) which has a dependency on an outdated version 1.8.0)

I think it's this one:

androidx.security:security-crypto:1.1.0-alpha06
+--- androidx.annotation:annotation:1.1.0 -> 1.9.1 (*)
+--- androidx.annotation:annotation:1.2.0 -> 1.9.1 (*)
+--- androidx.collection:collection:1.1.0 -> 1.4.4 (*)
\--- com.google.crypto.tink:tink-android:1.8.0

... which is deprecated but we use to encrypted the session data. Yay 🥲 .

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

testImplementation(libs.coroutines.test)
testImplementation(libs.test.junit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class DefaultRegisterUnifiedPushUseCase @Inject constructor(
UnifiedPush.saveDistributor(context, distributor.value)
// This will trigger the callback
// VectorUnifiedPushMessagingReceiver.onNewEndpoint
UnifiedPush.registerApp(context = context, instance = clientSecret)
UnifiedPush.register(context = context, instance = clientSecret)
// Wait for VectorUnifiedPushMessagingReceiver.onNewEndpoint to proceed
return runCatching {
withTimeout(30.seconds) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ class DefaultUnregisterUnifiedPushUseCase @Inject constructor(
override fun cleanup(clientSecret: String) {
unifiedPushStore.storeUpEndpoint(clientSecret, null)
unifiedPushStore.storePushGateway(clientSecret, null)
UnifiedPush.unregisterApp(context, clientSecret)
UnifiedPush.unregister(context, clientSecret)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import io.element.android.libraries.pushproviders.unifiedpush.registration.Endpo
import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.unifiedpush.android.connector.FailedReason
import org.unifiedpush.android.connector.MessagingReceiver
import org.unifiedpush.android.connector.data.PushEndpoint
import org.unifiedpush.android.connector.data.PushMessage
import timber.log.Timber
import javax.inject.Inject

Expand Down Expand Up @@ -45,15 +48,15 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
* @param message the message
* @param instance connection, for multi-account
*/
override fun onMessage(context: Context, message: ByteArray, instance: String) {
Timber.tag(loggerTag.value).w("New message")
override fun onMessage(context: Context, message: PushMessage, instance: String) {
Timber.tag(loggerTag.value).d("New message, decrypted: ${message.decrypted}")
coroutineScope.launch {
val pushData = pushParser.parse(message, instance)
val pushData = pushParser.parse(message.content, instance)
if (pushData == null) {
Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush")
pushHandler.handleInvalid(
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
data = String(message),
data = String(message.content),
)
} else {
pushHandler.handle(
Expand All @@ -68,20 +71,20 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
* Called when a new endpoint is to be used for sending push messages.
* You should send the endpoint to your application server and sync for missing notifications.
*/
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
override fun onNewEndpoint(context: Context, endpoint: PushEndpoint, instance: String) {
Timber.tag(loggerTag.value).w("onNewEndpoint: $endpoint")
coroutineScope.launch {
val gateway = unifiedPushGatewayResolver.getGateway(endpoint)
val gateway = unifiedPushGatewayResolver.getGateway(endpoint.url)
.let { gatewayResult ->
unifiedPushGatewayUrlResolver.resolve(gatewayResult, instance)
}
unifiedPushStore.storePushGateway(instance, gateway)
val result = newGatewayHandler.handle(endpoint, gateway, instance)
val result = newGatewayHandler.handle(endpoint.url, gateway, instance)
.onFailure {
Timber.tag(loggerTag.value).e(it, "Failed to handle new gateway")
}
.onSuccess {
unifiedPushStore.storeUpEndpoint(instance, endpoint)
unifiedPushStore.storeUpEndpoint(instance, endpoint.url)
}
endpointRegistrationHandler.registrationDone(
RegistrationResult(
Expand All @@ -96,8 +99,8 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
/**
* 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")
override fun onRegistrationFailed(context: Context, reason: FailedReason, instance: String) {
Timber.tag(loggerTag.value).e("onRegistrationFailed for $instance, reason: $reason")
/*
Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show()
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
Expand All @@ -110,7 +113,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
* Called when this application is unregistered from receiving push messages.
*/
override fun onUnregistered(context: Context, instance: String) {
Timber.tag(loggerTag.value).w("Unifiedpush: Unregistered")
Timber.tag(loggerTag.value).w("UnifiedPush: Unregistered")
/*
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
pushDataStore.setFdroidSyncBackgroundMode(mode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.unifiedpush.android.connector.FailedReason
import org.unifiedpush.android.connector.data.PublicKeySet
import org.unifiedpush.android.connector.data.PushEndpoint
import org.unifiedpush.android.connector.data.PushMessage

@RunWith(RobolectricTestRunner::class)
class VectorUnifiedPushMessagingReceiverTest {
Expand All @@ -56,7 +60,7 @@ class VectorUnifiedPushMessagingReceiverTest {
fun `onRegistrationFailed does nothing`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver()
vectorUnifiedPushMessagingReceiver.onRegistrationFailed(context, A_SECRET)
vectorUnifiedPushMessagingReceiver.onRegistrationFailed(context, FailedReason.NETWORK, A_SECRET)
}

@Test
Expand All @@ -68,7 +72,7 @@ class VectorUnifiedPushMessagingReceiverTest {
handleResult = pushHandlerResult
),
)
vectorUnifiedPushMessagingReceiver.onMessage(context, UnifiedPushParserTest.UNIFIED_PUSH_DATA.toByteArray(), A_SECRET)
vectorUnifiedPushMessagingReceiver.onMessage(context, aPushMessage(), A_SECRET)
advanceUntilIdle()
pushHandlerResult.assertions()
.isCalledOnce()
Expand Down Expand Up @@ -96,7 +100,7 @@ class VectorUnifiedPushMessagingReceiverTest {
handleInvalidResult = handleInvalidResult,
),
)
vectorUnifiedPushMessagingReceiver.onMessage(context, "".toByteArray(), A_SECRET)
vectorUnifiedPushMessagingReceiver.onMessage(context, aPushMessage(""), A_SECRET)
advanceUntilIdle()
handleInvalidResult.assertions().isCalledOnce()
}
Expand Down Expand Up @@ -127,7 +131,7 @@ class VectorUnifiedPushMessagingReceiverTest {
unifiedPushNewGatewayHandler = unifiedPushNewGatewayHandler,
)
endpointRegistrationHandler.state.test {
vectorUnifiedPushMessagingReceiver.onNewEndpoint(context, "anEndpoint", A_SECRET)
vectorUnifiedPushMessagingReceiver.onNewEndpoint(context, aPushEndpoint("anEndpoint"), A_SECRET)
advanceUntilIdle()
assertThat(awaitItem()).isEqualTo(
RegistrationResult(
Expand Down Expand Up @@ -170,7 +174,7 @@ class VectorUnifiedPushMessagingReceiverTest {
unifiedPushNewGatewayHandler = unifiedPushNewGatewayHandler,
)
endpointRegistrationHandler.state.test {
vectorUnifiedPushMessagingReceiver.onNewEndpoint(context, "anEndpoint", A_SECRET)
vectorUnifiedPushMessagingReceiver.onNewEndpoint(context, aPushEndpoint(), A_SECRET)
advanceUntilIdle()
assertThat(awaitItem()).isEqualTo(
RegistrationResult(
Expand Down Expand Up @@ -207,3 +211,19 @@ class VectorUnifiedPushMessagingReceiverTest {
}
}
}

private fun aPushMessage(
data: String = UnifiedPushParserTest.UNIFIED_PUSH_DATA,
decrypted: Boolean = true,
) = PushMessage(
content = data.toByteArray(),
decrypted = decrypted,
)

private fun aPushEndpoint(
url: String = "anEndpoint",
pubKeySet: PublicKeySet? = null,
) = PushEndpoint(
url = url,
pubKeySet = pubKeySet,
)
1 change: 0 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ dependencyResolutionManagement {
maven {
url = URI("https://www.jitpack.io")
content {
includeModule("com.github.UnifiedPush", "android-connector")
includeModule("com.github.matrix-org", "matrix-analytics-events")
}
}
Expand Down
Loading