From d49ef1dd7d4ffd629cd976b6360c6e3203154c97 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 18 Sep 2025 16:21:32 -0300 Subject: [PATCH] Convert RecipientId to Kotlin. --- .../securesms/recipients/RecipientId.java | 238 ------------------ .../securesms/recipients/RecipientId.kt | 183 ++++++++++++++ .../serialization/RecipientIdSerializer.kt | 29 --- .../GroupsV2UpdateMessageProducerTest.kt | 3 +- 4 files changed, 184 insertions(+), 269 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/serialization/RecipientIdSerializer.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java deleted file mode 100644 index 4cd64d391b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java +++ /dev/null @@ -1,238 +0,0 @@ -package org.thoughtcrime.securesms.recipients; - -import android.annotation.SuppressLint; -import android.os.Parcel; -import android.os.Parcelable; - -import androidx.annotation.AnyThread; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.annimon.stream.Stream; - -import org.signal.core.util.DatabaseId; -import org.signal.core.util.LongSerializer; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.util.DelimiterUtil; -import org.thoughtcrime.securesms.util.Util; -import org.whispersystems.signalservice.api.push.ServiceId; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.regex.Pattern; - -public class RecipientId implements Parcelable, Comparable, DatabaseId { - - private static final String TAG = "RecipientId"; - private static final long UNKNOWN_ID = -1; - private static final char DELIMITER = ','; - - public static final RecipientId UNKNOWN = RecipientId.from(UNKNOWN_ID); - public static final LongSerializer SERIALIZER = new Serializer(); - - private final long id; - - public static RecipientId from(long id) { - if (id == 0) { - throw new InvalidLongRecipientIdError(); - } - - return new RecipientId(id); - } - - public static RecipientId from(@NonNull String id) { - try { - return RecipientId.from(Long.parseLong(id)); - } catch (NumberFormatException e) { - throw new InvalidStringRecipientIdError(); - } - } - - public static @Nullable RecipientId fromNullable(@Nullable String id) { - return id != null ? from(id) : null; - } - - @AnyThread - public static @NonNull RecipientId from(@NonNull SignalServiceAddress address) { - return from(address.getServiceId(), address.getNumber().orElse(null)); - } - - @AnyThread - public static @NonNull RecipientId from(@NonNull ServiceId serviceId) { - return from(serviceId, null); - } - - @AnyThread - public static @NonNull RecipientId fromE164(@NonNull String identifier) { - return from(null, identifier); - } - - public static @NonNull RecipientId from(@NonNull GroupId groupId) { - RecipientId recipientId = RecipientIdCache.INSTANCE.get(groupId); - if (recipientId == null) { - Log.d(TAG, "RecipientId cache miss for " + groupId); - recipientId = SignalDatabase.recipients().getOrInsertFromPossiblyMigratedGroupId(groupId); - if (groupId.isV2()) { - RecipientIdCache.INSTANCE.put(groupId, recipientId); - } - } - return recipientId; - } - /** - * Used for when you have a string that could be either a UUID or an e164. This was primarily - * created for interacting with protocol stores. - * @param identifier A UUID or e164 - */ - @AnyThread - public static @NonNull RecipientId fromSidOrE164(@NonNull String identifier) { - ServiceId serviceId = ServiceId.parseOrNull(identifier); - if (serviceId != null) { - return from(serviceId); - } else { - return from(null, identifier); - } - } - - @AnyThread - @SuppressLint("WrongThread") - private static @NonNull RecipientId from(@Nullable ServiceId serviceId, @Nullable String e164) { - if (serviceId != null && serviceId.isUnknown()) { - return RecipientId.UNKNOWN; - } - - RecipientId recipientId = RecipientIdCache.INSTANCE.get(serviceId, e164); - - if (recipientId == null) { - recipientId = SignalDatabase.recipients().getAndPossiblyMerge(serviceId, e164); - RecipientIdCache.INSTANCE.put(recipientId, e164, serviceId); - } - - return recipientId; - } - - @AnyThread - public static void clearCache() { - RecipientIdCache.INSTANCE.clear(); - } - - private RecipientId(long id) { - this.id = id; - } - - private RecipientId(Parcel in) { - id = in.readLong(); - } - - public static @NonNull String toSerializedList(@NonNull Collection ids) { - return Util.join(Stream.of(ids).map(RecipientId::serialize).toList(), String.valueOf(DELIMITER)); - } - - public static List fromSerializedList(@NonNull String serialized) { - String[] stringIds = DelimiterUtil.split(serialized, DELIMITER); - List out = new ArrayList<>(stringIds.length); - - for (String stringId : stringIds) { - RecipientId id = RecipientId.from(Long.parseLong(stringId)); - out.add(id); - } - - return out; - } - - public static boolean serializedListContains(@NonNull String serialized, @NonNull RecipientId recipientId) { - return Pattern.compile("\\b" + recipientId.serialize() + "\\b") - .matcher(serialized) - .find(); - } - - public boolean isUnknown() { - return id == UNKNOWN_ID; - } - - @Override - public @NonNull String serialize() { - return String.valueOf(id); - } - - public long toLong() { - return id; - } - - public @NonNull String toQueueKey() { - return toQueueKey(false); - } - - public @NonNull String toQueueKey(boolean forMedia) { - return "RecipientId::" + id + (forMedia ? "::MEDIA" : ""); - } - - public @NonNull String toScheduledSendQueueKey() { - return "RecipientId::" + id + "::SCHEDULED"; - } - - @Override - public @NonNull String toString() { - return "RecipientId::" + id; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - RecipientId that = (RecipientId) o; - - return id == that.id; - } - - @Override - public int hashCode() { - return (int) (id ^ (id >>> 32)); - } - - @Override - public int compareTo(RecipientId o) { - return Long.compare(this.id, o.id); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeLong(id); - } - - public static final Creator CREATOR = new Creator() { - @Override - public RecipientId createFromParcel(Parcel in) { - return new RecipientId(in); - } - - @Override - public RecipientId[] newArray(int size) { - return new RecipientId[size]; - } - }; - - private static class InvalidLongRecipientIdError extends AssertionError {} - private static class InvalidStringRecipientIdError extends AssertionError {} - - private static class Serializer implements LongSerializer { - @Override - public Long serialize(RecipientId data) { - return data.toLong(); - } - - @Override - public @NonNull RecipientId deserialize(Long data) { - return RecipientId.from(data); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.kt new file mode 100644 index 0000000000..b45c66a773 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.recipients + +import android.annotation.SuppressLint +import android.os.Parcelable +import androidx.annotation.AnyThread +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import org.signal.core.util.DatabaseId +import org.signal.core.util.LongSerializer +import org.signal.core.util.logging.Log +import org.signal.core.util.orNull +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.groups.GroupId +import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.api.push.SignalServiceAddress +import java.util.regex.Pattern + +@Parcelize +@Serializable +class RecipientId private constructor(private val id: Long) : Parcelable, Comparable, DatabaseId { + + companion object { + + private val TAG = Log.tag(RecipientId::class) + private const val UNKNOWN_ID = -1L + private const val DELIMITER = ',' + + @JvmField + val UNKNOWN = from(UNKNOWN_ID) + + @JvmField + val SERIALIZER: LongSerializer = Serializer() + + @JvmStatic + fun from(id: Long): RecipientId { + if (id == 0L) { + throw InvalidLongRecipientIdError() + } + + return RecipientId(id) + } + + @JvmStatic + fun from(id: String): RecipientId { + try { + return from(id.toLong()) + } catch (_: NumberFormatException) { + throw InvalidStringRecipientIdError() + } + } + + @JvmStatic + fun fromNullable(id: String?): RecipientId? = id?.let { from(it) } + + @JvmStatic + fun from(address: SignalServiceAddress): RecipientId = from(address.serviceId, address.number.orNull()) + + @JvmStatic + fun from(serviceId: ServiceId): RecipientId = from(serviceId, null) + + @JvmStatic + fun fromE164(identifier: String): RecipientId = from(null, identifier) + + @JvmStatic + fun from(groupId: GroupId): RecipientId { + var recipientId = RecipientIdCache.INSTANCE.get(groupId) + if (recipientId == null) { + Log.d(TAG, "RecipientId cache miss for $groupId") + recipientId = SignalDatabase.recipients.getOrInsertFromPossiblyMigratedGroupId(groupId) + if (groupId.isV2) { + RecipientIdCache.INSTANCE.put(groupId, recipientId) + } + } + + return recipientId + } + + /** + * Used for when you have a string that could be either a UUID or an e164. This was primarily + * created for interacting with protocol stores. + * @param identifier A UUID or e164 + */ + @JvmStatic + @AnyThread + fun fromSidOrE164(identifier: String): RecipientId { + val serviceId = ServiceId.parseOrNull(identifier) + return if (serviceId != null) { + from(serviceId) + } else { + from(null, identifier) + } + } + + @JvmStatic + @AnyThread + @SuppressLint("WrongThread") + fun from(serviceId: ServiceId?, e164: String?): RecipientId { + if (serviceId != null && serviceId.isUnknown) { + return UNKNOWN + } + + var recipientId = RecipientIdCache.INSTANCE.get(serviceId, e164) + if (recipientId == null) { + recipientId = SignalDatabase.recipients.getAndPossiblyMerge(serviceId, e164) + RecipientIdCache.INSTANCE.put(recipientId, e164, serviceId) + } + + return recipientId + } + + @JvmStatic + @AnyThread + fun clearCache() { + RecipientIdCache.INSTANCE.clear() + } + + @JvmStatic + fun toSerializedList(ids: Collection): String { + return ids.joinToString(DELIMITER.toString()) { it.serialize() } + } + + @JvmStatic + fun fromSerializedList(serialized: String): List { + return serialized.split(DELIMITER).filter { it.isNotEmpty() }.map { from(it) } + } + + @JvmStatic + fun serializedListContains(serialized: String, recipientId: RecipientId): Boolean { + return Pattern.compile("\\b${recipientId.serialize()}\\b") + .matcher(serialized) + .find() + } + } + + override fun serialize(): String = id.toString() + + override fun toString(): String = "RecipientId::$id" + + fun toLong(): Long = id + + @Transient + @IgnoredOnParcel + val isUnknown: Boolean = id == UNKNOWN_ID + + @JvmOverloads + fun toQueueKey(forMedia: Boolean = false): String { + return "RecipientId::$id${if (forMedia) "::MEDIA" else ""}" + } + + fun toScheduledSendQueueKey(): String = "RecipientId::$id::SCHEDULED" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RecipientId + + return id == other.id + } + + override fun hashCode(): Int { + return id.hashCode() + } + + override fun compareTo(other: RecipientId): Int { + return id.compareTo(other.id) + } + + private class InvalidLongRecipientIdError : AssertionError() + private class InvalidStringRecipientIdError : AssertionError() + + private class Serializer : LongSerializer { + override fun serialize(data: RecipientId): Long = data.toLong() + override fun deserialize(input: Long): RecipientId = from(input) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/serialization/RecipientIdSerializer.kt b/app/src/main/java/org/thoughtcrime/securesms/serialization/RecipientIdSerializer.kt deleted file mode 100644 index d608130604..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/serialization/RecipientIdSerializer.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2025 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.serialization - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import org.thoughtcrime.securesms.recipients.RecipientId - -/** - * Kotlinx Serialization serializer for [RecipientId] objects. - */ -class RecipientIdSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("RecipientId", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: RecipientId) { - encoder.encodeString(value.serialize()) - } - - override fun deserialize(decoder: Decoder): RecipientId { - return RecipientId.from(decoder.decodeString()) - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.kt index 8f08fc6985..e426ecaa90 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.kt @@ -5,7 +5,6 @@ import androidx.test.core.app.ApplicationProvider import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject -import io.mockk.mockkStatic import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -50,7 +49,7 @@ class GroupsV2UpdateMessageProducerTest { @Before fun setup() { - mockkStatic(RecipientId::class) + mockkObject(RecipientId.Companion) val aliceId = RecipientId.from(1) val bobId = RecipientId.from(2) every { RecipientId.from(alice) } returns aliceId