Show the account a UnifiedPush registration belongs to in the distributor

A distributor such as ntfy or DAVx5 lists every registration it holds, but
Element registered each one with no message for the distributor, so an
account added from a second session was indistinguishable from the first.

RegisterUnifiedPushUseCase.execute now takes the session id and passes it
as messageForDistributor, which the connector has accepted since 3.x. The
value is the raw Matrix ID, which is not personal data by the project's
own logging rules, and no new string resource is needed.

UnifiedPushProvider already has the MatrixClient in hand at the only call
site, so the PushProvider interface is unchanged and nothing outside the
module moves.

Relates to https://github.com/element-hq/element-x-android/issues/6426
This commit is contained in:
hayaksi1
2026-08-09 03:51:20 +03:00
parent 044737a3a7
commit de0e628512
5 changed files with 20 additions and 14 deletions
@@ -12,6 +12,7 @@ import android.content.Context
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler
import kotlinx.coroutines.flow.filter
@@ -21,7 +22,7 @@ import org.unifiedpush.android.connector.UnifiedPush
import kotlin.time.Duration.Companion.seconds
interface RegisterUnifiedPushUseCase {
suspend fun execute(distributor: Distributor, clientSecret: String): Result<Unit>
suspend fun execute(distributor: Distributor, clientSecret: String, sessionId: SessionId): Result<Unit>
}
@ContributesBinding(AppScope::class)
@@ -29,11 +30,14 @@ class DefaultRegisterUnifiedPushUseCase(
@ApplicationContext private val context: Context,
private val endpointRegistrationHandler: EndpointRegistrationHandler,
) : RegisterUnifiedPushUseCase {
override suspend fun execute(distributor: Distributor, clientSecret: String): Result<Unit> {
override suspend fun execute(distributor: Distributor, clientSecret: String, sessionId: SessionId): Result<Unit> {
UnifiedPush.saveDistributor(context, distributor.value)
// This will trigger the callback
// VectorUnifiedPushMessagingReceiver.onNewEndpoint
UnifiedPush.register(context = context, instance = clientSecret)
// The session id is passed as the message for the distributor, so that its UI can tell one
// registration from another when several accounts are registered.
// See https://github.com/element-hq/element-x-android/issues/6426
UnifiedPush.register(context = context, instance = clientSecret, messageForDistributor = sessionId.value)
// Wait for VectorUnifiedPushMessagingReceiver.onNewEndpoint to proceed
@Suppress("RunCatchingNotAllowed")
return runCatching {
@@ -36,7 +36,7 @@ class UnifiedPushProvider(
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor): Result<Unit> {
val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId)
return registerUnifiedPushUseCase.execute(distributor, clientSecret)
return registerUnifiedPushUseCase.execute(distributor, clientSecret, matrixClient.sessionId)
.onSuccess {
unifiedPushStore.setDistributorValue(matrixClient.sessionId, distributor.value)
}
@@ -12,6 +12,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_SECRET
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler
import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult
@@ -41,7 +42,7 @@ class DefaultRegisterUnifiedPushUseCaseTest : RobolectricTest() {
delay(100)
endpointRegistrationHandler.registrationDone(RegistrationResult(A_SECRET, Result.success(Unit)))
}
val result = useCase.execute(aDistributor, A_SECRET)
val result = useCase.execute(aDistributor, A_SECRET, A_SESSION_ID)
assertThat(result.isSuccess).isTrue()
}
@@ -56,7 +57,7 @@ class DefaultRegisterUnifiedPushUseCaseTest : RobolectricTest() {
delay(100)
endpointRegistrationHandler.registrationDone(RegistrationResult(A_SECRET, Result.failure(AN_EXCEPTION)))
}
val result = useCase.execute(aDistributor, A_SECRET)
val result = useCase.execute(aDistributor, A_SECRET, A_SESSION_ID)
assertThat(result.isSuccess).isFalse()
}
@@ -67,7 +68,7 @@ class DefaultRegisterUnifiedPushUseCaseTest : RobolectricTest() {
endpointRegistrationHandler = endpointRegistrationHandler
)
val aDistributor = Distributor("aValue", "aName")
val result = useCase.execute(aDistributor, A_SECRET)
val result = useCase.execute(aDistributor, A_SECRET, A_SESSION_ID)
assertThat(result.isSuccess).isFalse()
}
@@ -8,13 +8,14 @@
package io.element.android.libraries.pushproviders.unifiedpush
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.tests.testutils.lambda.lambdaError
class FakeRegisterUnifiedPushUseCase(
private val result: (Distributor, String) -> Result<Unit> = { _, _ -> lambdaError() }
private val result: (Distributor, String, SessionId) -> Result<Unit> = { _, _, _ -> lambdaError() }
) : RegisterUnifiedPushUseCase {
override suspend fun execute(distributor: Distributor, clientSecret: String): Result<Unit> {
return result(distributor, clientSecret)
override suspend fun execute(distributor: Distributor, clientSecret: String, sessionId: SessionId): Result<Unit> {
return result(distributor, clientSecret, sessionId)
}
}
@@ -62,7 +62,7 @@ class UnifiedPushProviderTest {
@Test
fun `register ok`() = runTest {
val getSecretForUserResultLambda = lambdaRecorder<SessionId, String> { A_SECRET }
val executeLambda = lambdaRecorder<Distributor, String, Result<Unit>> { _, _ -> Result.success(Unit) }
val executeLambda = lambdaRecorder<Distributor, String, SessionId, Result<Unit>> { _, _, _ -> Result.success(Unit) }
val setDistributorValueResultLambda = lambdaRecorder<UserId, String, Unit> { _, _ -> }
val unifiedPushProvider = createUnifiedPushProvider(
pushClientSecret = FakePushClientSecret(
@@ -82,7 +82,7 @@ class UnifiedPushProviderTest {
.with(value(A_SESSION_ID))
executeLambda.assertions()
.isCalledOnce()
.with(value(Distributor("value", "Name")), value(A_SECRET))
.with(value(Distributor("value", "Name")), value(A_SECRET), value(A_SESSION_ID))
setDistributorValueResultLambda.assertions()
.isCalledOnce()
.with(value(A_SESSION_ID), value("value"))
@@ -91,7 +91,7 @@ class UnifiedPushProviderTest {
@Test
fun `register ko`() = runTest {
val getSecretForUserResultLambda = lambdaRecorder<SessionId, String> { A_SECRET }
val executeLambda = lambdaRecorder<Distributor, String, Result<Unit>> { _, _ -> Result.failure(AN_EXCEPTION) }
val executeLambda = lambdaRecorder<Distributor, String, SessionId, Result<Unit>> { _, _, _ -> Result.failure(AN_EXCEPTION) }
val setDistributorValueResultLambda = lambdaRecorder<UserId, String, Unit>(ensureNeverCalled = true) { _, _ -> }
val unifiedPushProvider = createUnifiedPushProvider(
pushClientSecret = FakePushClientSecret(
@@ -111,7 +111,7 @@ class UnifiedPushProviderTest {
.with(value(A_SESSION_ID))
executeLambda.assertions()
.isCalledOnce()
.with(value(Distributor("value", "Name")), value(A_SECRET))
.with(value(Distributor("value", "Name")), value(A_SECRET), value(A_SESSION_ID))
}
@Test