Migrate to libsignal setPushToken/clearPushToken.

Co-authored-by: Cody Henthorne <cody@signal.org>
# Conflicts:
#	lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountApi.kt
This commit is contained in:
andrew-signal
2026-07-17 11:45:10 -04:00
committed by Greyson Parrelli
parent abf05293eb
commit 7e79dfa57f
5 changed files with 77 additions and 51 deletions
@@ -25,7 +25,6 @@ import org.signal.core.ui.BottomSheetUtil
import org.signal.core.ui.permissions.PermissionDeniedBottomSheet
import org.signal.core.ui.permissions.RationaleDialog
import org.signal.core.util.AppUtil
import org.signal.core.util.ThreadUtil
import org.signal.core.util.Util
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.concurrent.SimpleTask
@@ -1055,19 +1054,17 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
.setTitle("Unregister?")
.setMessage("Are you sure? You'll have to re-register to use Signal again -- no promises that the process will go smoothly.")
.setPositiveButton(android.R.string.ok) { _, _ ->
AdvancedPrivacySettingsRepository(requireContext()).disablePushMessages {
ThreadUtil.runOnMain {
when (it) {
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.SUCCESS -> {
SignalStore.account.setRegistered(false)
SignalStore.registration.clearRegistrationComplete()
SignalStore.registration.hasUploadedProfile = false
Toast.makeText(context, "Unregistered!", Toast.LENGTH_SHORT).show()
}
lifecycleScope.launch {
when (AdvancedPrivacySettingsRepository(requireContext()).disablePushMessages()) {
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.SUCCESS -> {
SignalStore.account.setRegistered(false)
SignalStore.registration.clearRegistrationComplete()
SignalStore.registration.hasUploadedProfile = false
Toast.makeText(context, "Unregistered!", Toast.LENGTH_SHORT).show()
}
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.NETWORK_ERROR -> {
Toast.makeText(context, "Network error!", Toast.LENGTH_SHORT).show()
}
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.NETWORK_ERROR -> {
Toast.makeText(context, "Network error!", Toast.LENGTH_SHORT).show()
}
}
}
@@ -3,8 +3,12 @@ package org.thoughtcrime.securesms.components.settings.app.privacy.advanced
import android.content.Context
import com.google.android.gms.tasks.Tasks
import com.google.firebase.installations.FirebaseInstallations
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.signal.libsignal.net.DeviceDeregisteredException
import org.signal.libsignal.net.RequestResult
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
@@ -13,8 +17,6 @@ import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.NetworkResultUtil
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException
import java.io.IOException
import java.util.concurrent.ExecutionException
@@ -22,30 +24,34 @@ private val TAG = Log.tag(AdvancedPrivacySettingsRepository::class.java)
class AdvancedPrivacySettingsRepository(private val context: Context) {
fun disablePushMessages(consumer: (DisablePushMessagesResult) -> Unit) {
SignalExecutors.BOUNDED.execute {
val result = try {
try {
NetworkResultUtil.toBasicLegacy(SignalNetwork.account.clearFcmToken())
} catch (e: AuthorizationFailedException) {
Log.w(TAG, e)
}
if (SignalStore.account.fcmEnabled) {
Tasks.await(FirebaseInstallations.getInstance().delete())
}
DisablePushMessagesResult.SUCCESS
} catch (ioe: IOException) {
Log.w(TAG, ioe)
DisablePushMessagesResult.NETWORK_ERROR
} catch (e: InterruptedException) {
Log.w(TAG, "Interrupted while deleting", e)
DisablePushMessagesResult.NETWORK_ERROR
} catch (e: ExecutionException) {
Log.w(TAG, "Error deleting", e.cause)
DisablePushMessagesResult.NETWORK_ERROR
}
suspend fun disablePushMessages(): DisablePushMessagesResult = withContext(Dispatchers.IO) {
val clearTokenError: Throwable? = when (val result = SignalNetwork.account.clearFcmToken()) {
is RequestResult.Success, is RequestResult.NonSuccess -> null
is RequestResult.RetryableNetworkError -> result.networkError
is RequestResult.ApplicationError -> result.cause
}
consumer(result)
if (clearTokenError != null) {
Log.w(TAG, clearTokenError)
if (clearTokenError !is DeviceDeregisteredException) {
return@withContext DisablePushMessagesResult.NETWORK_ERROR
}
}
try {
if (SignalStore.account.fcmEnabled) {
Tasks.await(FirebaseInstallations.getInstance().delete())
}
DisablePushMessagesResult.SUCCESS
} catch (ioe: IOException) {
Log.w(TAG, ioe)
DisablePushMessagesResult.NETWORK_ERROR
} catch (e: InterruptedException) {
Log.w(TAG, "Interrupted while deleting", e)
DisablePushMessagesResult.NETWORK_ERROR
} catch (e: ExecutionException) {
Log.w(TAG, "Error deleting", e.cause)
DisablePushMessagesResult.NETWORK_ERROR
}
}
@@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.net.SignalNetwork;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.signal.core.util.PlayServicesUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import org.whispersystems.signalservice.api.RequestResultUtil;
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
import java.io.IOException;
@@ -101,7 +101,7 @@ public class FcmRefreshJob extends BaseJob {
Log.i(TAG, "Token didn't change.");
}
NetworkResultUtil.toBasicLegacy(SignalNetwork.account().setFcmToken(token.get()));
RequestResultUtil.successOrThrowNoError(SignalNetwork.account().setFcmToken(token.get()));
SignalStore.account().setFcmToken(token.get());
if (!SignalStore.account().isFcmEnabled()) {
@@ -70,6 +70,25 @@ fun <T : Any> RequestResult<T, RestStatusCodeError>.successOrThrow(): T {
}
}
/**
* [successOrThrow] variant for an API that defines no business-logic errors, so there is no
* [RequestResult.NonSuccess] case to handle. Exists as a separate overload because Java cannot
* bind a [Nothing] error argument to the [RestStatusCodeError] receiver above.
*/
@JvmName("successOrThrowNoError")
@Throws(IOException::class)
fun <T : Any> RequestResult<T, Nothing>.successOrThrow(): T {
return when (this) {
is RequestResult.Success -> result
is RequestResult.RetryableNetworkError -> throw networkError
is RequestResult.NonSuccess -> error("Branch is unreachable")
is RequestResult.ApplicationError -> throw when (val error = cause) {
is IOException, is RuntimeException -> error
else -> RuntimeException(error)
}
}
}
private fun <T : Any> WebsocketResponse.toRequestResult(clazz: KClass<T>): RequestResult<T, RestStatusCodeError> {
return if (status < 200 || status > 299) {
RequestResult.NonSuccess(RestStatusCodeError(status, headers, body?.toByteArray()))
@@ -5,8 +5,10 @@
package org.whispersystems.signalservice.api.account
import kotlinx.coroutines.runBlocking
import org.signal.core.util.Base64
import org.signal.core.util.Base64.encodeUrlSafeWithoutPadding
import org.signal.libsignal.net.AuthDevicesService
import org.signal.libsignal.net.RequestResult
import org.signal.libsignal.usernames.BaseUsernameException
import org.signal.libsignal.usernames.Username
@@ -21,7 +23,6 @@ import org.whispersystems.signalservice.api.push.UsernameLinkComponents
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
import org.whispersystems.signalservice.internal.push.ConfirmUsernameRequest
import org.whispersystems.signalservice.internal.push.ConfirmUsernameResponse
import org.whispersystems.signalservice.internal.push.GcmRegistrationId
import org.whispersystems.signalservice.internal.push.PhoneNumberDiscoverabilityRequest
import org.whispersystems.signalservice.internal.push.PushServiceSocket
import org.whispersystems.signalservice.internal.push.ReserveUsernameRequest
@@ -52,21 +53,24 @@ class AccountApi(private val authWebSocket: SignalWebSocket.AuthenticatedWebSock
}
/**
* PUT /v1/accounts/gcm
* - 200: Success
* Sets the FCM push token the server should use to notify this device of new messages.
*/
fun setFcmToken(fcmToken: String): NetworkResult<Unit> {
val request = WebSocketRequestMessage.put("/v1/accounts/gcm", GcmRegistrationId(fcmToken, true))
return NetworkResult.fromWebSocketRequest(authWebSocket, request)
fun setFcmToken(fcmToken: String): RequestResult<Unit, Nothing> {
return runBlocking {
authWebSocket.runCatchingWithChatConnection { connection ->
AuthDevicesService(connection).setPushToken(fcmToken)
}
}
}
/**
* DELETE /v1/account/gcm
* - 204: Success
* Removes any push tokens associated with this device. Afterwards, the server will assume this device
* polls for new messages over an open websocket.
*/
fun clearFcmToken(): NetworkResult<Unit> {
val request = WebSocketRequestMessage.delete("/v1/accounts/gcm")
return NetworkResult.fromWebSocketRequest(authWebSocket, request)
suspend fun clearFcmToken(): RequestResult<Unit, Nothing> {
return authWebSocket.runCatchingWithChatConnection { connection ->
AuthDevicesService(connection).clearPushToken()
}
}
/**