From ff882edeaec2c64cc061ffaa55e219eca25bf804 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Thu, 8 Dec 2022 10:28:24 -0500 Subject: [PATCH] Enable kotlin for libsignal-service project and convert SignalServiceDataMessage. --- ...ontentProcessor__handleStoryMessageTest.kt | 2 +- ...ContentProcessor__handleTextMessageTest.kt | 2 +- .../attachments/PointerAttachment.java | 2 +- .../messages/MessageContentProcessor.java | 5 +- build.gradle | 1 + gradle/verification-metadata.xml | 128 +++ libsignal/service/build.gradle | 4 + .../api/SignalServiceMessageSender.java | 30 +- .../api/messages/SignalServiceContent.java | 80 +- .../messages/SignalServiceDataMessage.java | 729 ------------------ .../api/messages/SignalServiceDataMessage.kt | 288 +++++++ .../signalservice/api/util/OptionalUtil.java | 53 -- .../signalservice/api/util/OptionalUtil.kt | 67 ++ 13 files changed, 553 insertions(+), 838 deletions(-) delete mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.kt delete mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.kt diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleStoryMessageTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleStoryMessageTest.kt index 4ddc5b298..ec4931137 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleStoryMessageTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleStoryMessageTest.kt @@ -176,6 +176,6 @@ class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorT private fun runTestWithContent(contentProto: SignalServiceContentProto) { val content = SignalServiceContent.createFromProto(contentProto) val testSubject = createNormalContentTestSubject() - testSubject.doProcess(content = content) + testSubject.doProcess(content = content!!) } } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleTextMessageTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleTextMessageTest.kt index 38552ce6f..5551932d7 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleTextMessageTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleTextMessageTest.kt @@ -20,7 +20,7 @@ class MessageContentProcessor__handleTextMessageTest : MessageContentProcessorTe val content = SignalServiceContent.createFromProto(contentProto) // WHEN - testSubject.doProcess(content = content) + testSubject.doProcess(content = content!!) // THEN val record = SignalDatabase.sms.getMessageRecord(1) diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java index 94f161bdf..009557120 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java @@ -68,7 +68,7 @@ public class PointerAttachment extends Attachment { return results; } - public static List forPointers(List pointers) { + public static List forPointers(@Nullable List pointers) { List results = new LinkedList<>(); if (pointers != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index 1b37a7344..5b9e3eb02 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -198,6 +198,7 @@ import java.util.concurrent.TimeUnit; * Takes data about a decrypted message, transforms it into user-presentable data, and writes that * data to our data stores. */ +@SuppressWarnings({ "OptionalGetWithoutIsPresent", "OptionalIsPresent" }) public final class MessageContentProcessor { private static final String TAG = Log.tag(MessageContentProcessor.class); @@ -616,7 +617,7 @@ public final class MessageContentProcessor { database.markAsMissedCall(smsMessageId.get(), message.getType() == OfferMessage.Type.VIDEO_CALL); } else { RemotePeer remotePeer = new RemotePeer(senderRecipient.getId(), new CallId(message.getId())); - byte[] remoteIdentityKey = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(senderRecipient.getId()).map(record -> record.getIdentityKey().serialize()).orElse(null); + byte[] remoteIdentityKey = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(senderRecipient.getId()).map(record -> record.getIdentityKey().serialize()).get(); ApplicationDependencies.getSignalCallManager() .receivedOffer(new WebRtcData.CallMetadata(remotePeer, content.getSenderDevice()), @@ -634,7 +635,7 @@ public final class MessageContentProcessor { { log(String.valueOf(content), "handleCallAnswerMessage..."); RemotePeer remotePeer = new RemotePeer(senderRecipient.getId(), new CallId(message.getId())); - byte[] remoteIdentityKey = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(senderRecipient.getId()).map(record -> record.getIdentityKey().serialize()).orElse(null); + byte[] remoteIdentityKey = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(senderRecipient.getId()).map(record -> record.getIdentityKey().serialize()).get(); ApplicationDependencies.getSignalCallManager() .receivedAnswer(new WebRtcData.CallMetadata(remotePeer, content.getSenderDevice()), diff --git a/build.gradle b/build.gradle index 946d118de..cd53100e3 100644 --- a/build.gradle +++ b/build.gradle @@ -79,6 +79,7 @@ task qa { ':Signal-Android:lintPlayProdRelease', 'Signal-Android:ktlintCheck', ':libsignal-service:test', + ':libsignal-service:ktlintCheck', ':Signal-Android:assemblePlayProdRelease' } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index b6a80016e..fc25766bb 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2292,6 +2292,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2300,6 +2308,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2308,6 +2324,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2316,6 +2340,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2324,6 +2356,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2332,6 +2372,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2340,6 +2388,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2348,6 +2404,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2356,6 +2420,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2364,6 +2436,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2372,6 +2452,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -3424,6 +3512,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3459,6 +3552,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3599,6 +3697,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3619,6 +3722,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3699,6 +3807,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3749,6 +3862,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3784,6 +3902,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3824,6 +3947,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + diff --git a/libsignal/service/build.gradle b/libsignal/service/build.gradle index 2216625f7..5d99b0ef2 100644 --- a/libsignal/service/build.gradle +++ b/libsignal/service/build.gradle @@ -1,9 +1,11 @@ apply plugin: 'java-library' +apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'java-test-fixtures' apply plugin: 'com.google.protobuf' apply plugin: 'maven-publish' apply plugin: 'signing' apply plugin: 'idea' +apply plugin: 'org.jlleitschuh.gradle.ktlint' sourceCompatibility = 1.8 archivesBaseName = "signal-service-java" @@ -41,6 +43,8 @@ dependencies { api libs.rxjava3.rxjava + implementation libs.kotlin.stdlib.jdk8 + testImplementation testLibs.junit.junit testImplementation testLibs.assertj.core testImplementation testLibs.conscrypt.openjdk.uber diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 2148f38e0..fb1a6bea1 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -924,8 +924,9 @@ public class SignalServiceMessageSender { .setAuthorUuid(message.getQuote().get().getAuthor().toString()) .setType(message.getQuote().get().getType().getProtoType()); - if (!message.getQuote().get().getMentions().isEmpty()) { - for (SignalServiceDataMessage.Mention mention : message.getQuote().get().getMentions()) { + List mentions = message.getQuote().get().getMentions(); + if (mentions != null && !mentions.isEmpty()) { + for (SignalServiceDataMessage.Mention mention : mentions) { quoteBuilder.addBodyRanges(DataMessage.BodyRange.newBuilder() .setStart(mention.getStart()) .setLength(mention.getLength()) @@ -935,20 +936,23 @@ public class SignalServiceMessageSender { builder.setRequiredProtocolVersion(Math.max(DataMessage.ProtocolVersion.MENTIONS_VALUE, builder.getRequiredProtocolVersion())); } - for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : message.getQuote().get().getAttachments()) { - DataMessage.Quote.QuotedAttachment.Builder quotedAttachment = DataMessage.Quote.QuotedAttachment.newBuilder(); + List attachments = message.getQuote().get().getAttachments(); + if (attachments != null) { + for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : attachments) { + DataMessage.Quote.QuotedAttachment.Builder quotedAttachment = DataMessage.Quote.QuotedAttachment.newBuilder(); - quotedAttachment.setContentType(attachment.getContentType()); + quotedAttachment.setContentType(attachment.getContentType()); - if (attachment.getFileName() != null) { - quotedAttachment.setFileName(attachment.getFileName()); + if (attachment.getFileName() != null) { + quotedAttachment.setFileName(attachment.getFileName()); + } + + if (attachment.getThumbnail() != null) { + quotedAttachment.setThumbnail(createAttachmentPointer(attachment.getThumbnail().asStream())); + } + + quoteBuilder.addAttachments(quotedAttachment); } - - if (attachment.getThumbnail() != null) { - quotedAttachment.setThumbnail(createAttachmentPointer(attachment.getThumbnail().asStream())); - } - - quoteBuilder.addAttachments(quotedAttachment); } builder.setQuote(quoteBuilder); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index 6cdda8678..032dc525f 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -69,7 +69,9 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -public final class SignalServiceContent { +import javax.annotation.Nullable; + +@SuppressWarnings("OptionalIsPresent") public final class SignalServiceContent { private static final String TAG = SignalServiceContent.class.getSimpleName(); @@ -492,7 +494,7 @@ public final class SignalServiceContent { return serializedState.toByteArray(); } - public static SignalServiceContent deserialize(byte[] data) { + public static @Nullable SignalServiceContent deserialize(byte[] data) { try { if (data == null) return null; @@ -508,7 +510,7 @@ public final class SignalServiceContent { /** * Takes internal protobuf serialization format and processes it into a {@link SignalServiceContent}. */ - public static SignalServiceContent createFromProto(SignalServiceContentProto serviceContentProto) + public static @Nullable SignalServiceContent createFromProto(SignalServiceContentProto serviceContentProto) throws ProtocolInvalidMessageException, ProtocolInvalidKeyException, UnsupportedDataMessageException, InvalidMessageStructureException { SignalServiceMetadata metadata = SignalServiceMetadataProtobufSerializer.fromProtobuf(serviceContentProto.getMetadata()); @@ -721,27 +723,29 @@ public final class SignalServiceContent { metadata.getSenderDevice()); } - return new SignalServiceDataMessage(metadata.getTimestamp(), - groupInfoV2, - attachments, - content.hasBody() ? content.getBody() : null, - endSession, - content.getExpireTimer(), - expirationUpdate, - content.hasProfileKey() ? content.getProfileKey().toByteArray() : null, - profileKeyUpdate, - quote, - sharedContacts, - previews, - mentions, - sticker, - content.getIsViewOnce(), - reaction, - remoteDelete, - groupCallUpdate, - payment, - storyContext, - giftBadge); + return SignalServiceDataMessage.newBuilder() + .withTimestamp(metadata.getTimestamp()) + .asGroupMessage(groupInfoV2) + .withAttachments(attachments) + .withBody(content.hasBody() ? content.getBody() : null) + .asEndSessionMessage(endSession) + .withExpiration(content.getExpireTimer()) + .asExpirationUpdate(expirationUpdate) + .withProfileKey(content.hasProfileKey() ? content.getProfileKey().toByteArray() : null) + .asProfileKeyUpdate(profileKeyUpdate) + .withQuote(quote) + .withSharedContacts(sharedContacts) + .withPreviews(previews) + .withMentions(mentions) + .withSticker(sticker) + .withViewOnce(content.getIsViewOnce()) + .withReaction(reaction) + .withRemoteDelete(remoteDelete) + .withGroupCallUpdate(groupCallUpdate) + .withPayment(payment) + .withStoryContext(storyContext) + .withGiftBadge(giftBadge) + .build(); } private static SignalServiceSyncMessage createSynchronizeMessage(SignalServiceMetadata metadata, @@ -1093,7 +1097,7 @@ public final class SignalServiceContent { } } - private static SignalServiceDataMessage.Quote createQuote(SignalServiceProtos.DataMessage content, boolean isGroupV2) + private static @Nullable SignalServiceDataMessage.Quote createQuote(SignalServiceProtos.DataMessage content, boolean isGroupV2) throws InvalidMessageStructureException { if (!content.hasQuote()) return null; @@ -1120,7 +1124,7 @@ public final class SignalServiceContent { } } - private static List createPreviews(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException { + private static @Nullable List createPreviews(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException { if (content.getPreviewCount() <= 0) return null; List results = new LinkedList<>(); @@ -1146,7 +1150,7 @@ public final class SignalServiceContent { Optional.ofNullable(attachment)); } - private static List createMentions(List bodyRanges, String body, boolean isGroupV2) + private static @Nullable List createMentions(List bodyRanges, String body, boolean isGroupV2) throws InvalidMessageStructureException { if (bodyRanges == null || bodyRanges.isEmpty() || body == null) { @@ -1172,7 +1176,7 @@ public final class SignalServiceContent { return mentions; } - private static SignalServiceDataMessage.Sticker createSticker(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException { + private static @Nullable SignalServiceDataMessage.Sticker createSticker(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException { if (!content.hasSticker() || !content.getSticker().hasPackId() || !content.getSticker().hasPackKey() || @@ -1191,7 +1195,7 @@ public final class SignalServiceContent { createAttachmentPointer(sticker.getData())); } - private static SignalServiceDataMessage.Reaction createReaction(SignalServiceProtos.DataMessage content) { + private static @Nullable SignalServiceDataMessage.Reaction createReaction(SignalServiceProtos.DataMessage content) { if (!content.hasReaction() || !content.getReaction().hasEmoji() || !content.getReaction().hasTargetAuthorUuid() || @@ -1214,7 +1218,7 @@ public final class SignalServiceContent { reaction.getTargetSentTimestamp()); } - private static SignalServiceDataMessage.RemoteDelete createRemoteDelete(SignalServiceProtos.DataMessage content) { + private static @Nullable SignalServiceDataMessage.RemoteDelete createRemoteDelete(SignalServiceProtos.DataMessage content) { if (!content.hasDelete() || !content.getDelete().hasTargetSentTimestamp()) { return null; } @@ -1224,7 +1228,7 @@ public final class SignalServiceContent { return new SignalServiceDataMessage.RemoteDelete(delete.getTargetSentTimestamp()); } - private static SignalServiceDataMessage.GroupCallUpdate createGroupCallUpdate(SignalServiceProtos.DataMessage content) { + private static @Nullable SignalServiceDataMessage.GroupCallUpdate createGroupCallUpdate(SignalServiceProtos.DataMessage content) { if (!content.hasGroupCallUpdate()) { return null; } @@ -1234,7 +1238,7 @@ public final class SignalServiceContent { return new SignalServiceDataMessage.GroupCallUpdate(groupCallUpdate.getEraId()); } - private static SignalServiceDataMessage.Payment createPayment(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException { + private static @Nullable SignalServiceDataMessage.Payment createPayment(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException { if (!content.hasPayment()) { return null; } @@ -1251,7 +1255,7 @@ public final class SignalServiceContent { } } - private static SignalServiceDataMessage.StoryContext createStoryContext(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException { + private static @Nullable SignalServiceDataMessage.StoryContext createStoryContext(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException { if (!content.hasStoryContext()) { return null; } @@ -1265,7 +1269,7 @@ public final class SignalServiceContent { return new SignalServiceDataMessage.StoryContext(serviceId, content.getStoryContext().getSentTimestamp()); } - private static SignalServiceDataMessage.GiftBadge createGiftBadge(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException { + private static @Nullable SignalServiceDataMessage.GiftBadge createGiftBadge(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException { if (!content.hasGiftBadge()) { return null; } @@ -1310,7 +1314,7 @@ public final class SignalServiceContent { return new SignalServiceDataMessage.PaymentActivation(payment.getType()); } - private static List createSharedContacts(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException { + private static @Nullable List createSharedContacts(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException { if (content.getContactCount() <= 0) return null; List results = new LinkedList<>(); @@ -1469,21 +1473,21 @@ public final class SignalServiceContent { } } - private static SignalServiceGroupV2 createGroupV2Info(SignalServiceProtos.StoryMessage storyMessage) throws InvalidMessageStructureException { + private static @Nullable SignalServiceGroupV2 createGroupV2Info(SignalServiceProtos.StoryMessage storyMessage) throws InvalidMessageStructureException { if (!storyMessage.hasGroup()) { return null; } return createGroupV2Info(storyMessage.getGroup()); } - private static SignalServiceGroupV2 createGroupV2Info(SignalServiceProtos.DataMessage dataMessage) throws InvalidMessageStructureException { + private static @Nullable SignalServiceGroupV2 createGroupV2Info(SignalServiceProtos.DataMessage dataMessage) throws InvalidMessageStructureException { if (!dataMessage.hasGroupV2()) { return null; } return createGroupV2Info(dataMessage.getGroupV2()); } - private static SignalServiceGroupV2 createGroupV2Info(SignalServiceProtos.GroupContextV2 groupV2) throws InvalidMessageStructureException { + private static @Nullable SignalServiceGroupV2 createGroupV2Info(SignalServiceProtos.GroupContextV2 groupV2) throws InvalidMessageStructureException { if (groupV2 == null) { return null; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java deleted file mode 100644 index 3c7b0273f..000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java +++ /dev/null @@ -1,729 +0,0 @@ -/* - * Copyright (C) 2014-2016 Open Whisper Systems - * - * Licensed according to the LICENSE file in this repository. - */ - -package org.whispersystems.signalservice.api.messages; - -import org.signal.libsignal.protocol.InvalidMessageException; -import org.signal.libsignal.zkgroup.groups.GroupSecretParams; -import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation; -import org.whispersystems.signalservice.api.messages.shared.SharedContact; -import org.whispersystems.signalservice.api.push.ServiceId; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.OptionalUtil; -import org.whispersystems.signalservice.internal.push.SignalServiceProtos; - -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; - -/** - * Represents a decrypted Signal Service data message. - */ -public class SignalServiceDataMessage { - - private final long timestamp; - private final Optional> attachments; - private final Optional body; - private final Optional group; - private final Optional profileKey; - private final boolean endSession; - private final boolean expirationUpdate; - private final int expiresInSeconds; - private final boolean profileKeyUpdate; - private final Optional quote; - private final Optional> contacts; - private final Optional> previews; - private final Optional> mentions; - private final Optional sticker; - private final boolean viewOnce; - private final Optional reaction; - private final Optional remoteDelete; - private final Optional groupCallUpdate; - private final Optional payment; - private final Optional storyContext; - private final Optional giftBadge; - - /** - * Construct a SignalServiceDataMessage. - * - * @param timestamp The sent timestamp. - * @param groupV2 The group information (or null if none). - * @param attachments The attachments (or null if none). - * @param body The message contents. - * @param endSession Flag indicating whether this message should close a session. - * @param expiresInSeconds Number of seconds in which the message should disappear after being seen. - */ - SignalServiceDataMessage(long timestamp, - SignalServiceGroupV2 groupV2, - List attachments, - String body, - boolean endSession, - int expiresInSeconds, - boolean expirationUpdate, - byte[] profileKey, - boolean profileKeyUpdate, - Quote quote, - List sharedContacts, - List previews, - List mentions, - Sticker sticker, - boolean viewOnce, - Reaction reaction, - RemoteDelete remoteDelete, - GroupCallUpdate groupCallUpdate, - Payment payment, - StoryContext storyContext, - GiftBadge giftBadge) - { - this.group = Optional.ofNullable(groupV2); - this.timestamp = timestamp; - this.body = OptionalUtil.absentIfEmpty(body); - this.endSession = endSession; - this.expiresInSeconds = expiresInSeconds; - this.expirationUpdate = expirationUpdate; - this.profileKey = Optional.ofNullable(profileKey); - this.profileKeyUpdate = profileKeyUpdate; - this.quote = Optional.ofNullable(quote); - this.sticker = Optional.ofNullable(sticker); - this.viewOnce = viewOnce; - this.reaction = Optional.ofNullable(reaction); - this.remoteDelete = Optional.ofNullable(remoteDelete); - this.groupCallUpdate = Optional.ofNullable(groupCallUpdate); - this.payment = Optional.ofNullable(payment); - this.storyContext = Optional.ofNullable(storyContext); - this.giftBadge = Optional.ofNullable(giftBadge); - - if (attachments != null && !attachments.isEmpty()) { - this.attachments = Optional.of(attachments); - } else { - this.attachments = Optional.empty(); - } - - if (sharedContacts != null && !sharedContacts.isEmpty()) { - this.contacts = Optional.of(sharedContacts); - } else { - this.contacts = Optional.empty(); - } - - if (previews != null && !previews.isEmpty()) { - this.previews = Optional.of(previews); - } else { - this.previews = Optional.empty(); - } - - if (mentions != null && !mentions.isEmpty()) { - this.mentions = Optional.of(mentions); - } else { - this.mentions = Optional.empty(); - } - } - - public static Builder newBuilder() { - return new Builder(); - } - - /** - * @return The message timestamp. - */ - public long getTimestamp() { - return timestamp; - } - - /** - * @return The message attachments (if any). - */ - public Optional> getAttachments() { - return attachments; - } - - /** - * @return The message body (if any). - */ - public Optional getBody() { - return body; - } - - /** - * @return The message group context (if any). - */ - public Optional getGroupContext() { - return group; - } - - public boolean isEndSession() { - return endSession; - } - - public boolean isExpirationUpdate() { - return expirationUpdate; - } - - public boolean isActivatePaymentsRequest() { - return getPayment().isPresent() && - getPayment().get().getPaymentActivation().isPresent() && - getPayment().get().getPaymentActivation().get().getType().equals(SignalServiceProtos.DataMessage.Payment.Activation.Type.REQUEST); - } - - public boolean isPaymentsActivated() { - return getPayment().isPresent() && - getPayment().get().getPaymentActivation().isPresent() && - getPayment().get().getPaymentActivation().get().getType().equals(SignalServiceProtos.DataMessage.Payment.Activation.Type.ACTIVATED); - } - - public boolean isProfileKeyUpdate() { - return profileKeyUpdate; - } - - public boolean isGroupV2Message() { - return group.isPresent(); - } - - public boolean isGroupV2Update() { - return group.isPresent() && - group.get().hasSignedGroupChange() && - !hasRenderableContent(); - } - - public boolean isEmptyGroupV2Message() { - return isGroupV2Message() && !isGroupV2Update() && !hasRenderableContent(); - } - - /** Contains some user data that affects the conversation */ - public boolean hasRenderableContent() { - return attachments.isPresent() || - body.isPresent() || - quote.isPresent() || - contacts.isPresent() || - previews.isPresent() || - mentions.isPresent() || - sticker.isPresent() || - reaction.isPresent() || - remoteDelete.isPresent(); - } - - public int getExpiresInSeconds() { - return expiresInSeconds; - } - - public Optional getProfileKey() { - return profileKey; - } - - public Optional getQuote() { - return quote; - } - - public Optional> getSharedContacts() { - return contacts; - } - - public Optional> getPreviews() { - return previews; - } - - public Optional> getMentions() { - return mentions; - } - - public Optional getSticker() { - return sticker; - } - - public boolean isViewOnce() { - return viewOnce; - } - - public Optional getReaction() { - return reaction; - } - - public Optional getRemoteDelete() { - return remoteDelete; - } - - public Optional getGroupCallUpdate() { - return groupCallUpdate; - } - - public Optional getPayment() { - return payment; - } - - public Optional getStoryContext() { - return storyContext; - } - - public Optional getGiftBadge() { - return giftBadge; - } - - public Optional getGroupId() { - byte[] groupId = null; - - if (getGroupContext().isPresent() && getGroupContext().isPresent()) { - SignalServiceGroupV2 gv2 = getGroupContext().get(); - groupId = GroupSecretParams.deriveFromMasterKey(gv2.getMasterKey()) - .getPublicParams() - .getGroupIdentifier() - .serialize(); - } - - return Optional.ofNullable(groupId); - } - - public static class Builder { - - private List attachments = new LinkedList<>(); - private List sharedContacts = new LinkedList<>(); - private List previews = new LinkedList<>(); - private List mentions = new LinkedList<>(); - - private long timestamp; - private SignalServiceGroupV2 groupV2; - private String body; - private boolean endSession; - private int expiresInSeconds; - private boolean expirationUpdate; - private byte[] profileKey; - private boolean profileKeyUpdate; - private Quote quote; - private Sticker sticker; - private boolean viewOnce; - private Reaction reaction; - private RemoteDelete remoteDelete; - private GroupCallUpdate groupCallUpdate; - private Payment payment; - private StoryContext storyContext; - private GiftBadge giftBadge; - - private Builder() {} - - public Builder withTimestamp(long timestamp) { - this.timestamp = timestamp; - return this; - } - - public Builder asGroupMessage(SignalServiceGroupV2 group) { - this.groupV2 = group; - return this; - } - - public Builder withAttachment(SignalServiceAttachment attachment) { - this.attachments.add(attachment); - return this; - } - - public Builder withAttachments(List attachments) { - this.attachments.addAll(attachments); - return this; - } - - public Builder withBody(String body) { - this.body = body; - return this; - } - - public Builder asEndSessionMessage() { - return asEndSessionMessage(true); - } - - public Builder asEndSessionMessage(boolean endSession) { - this.endSession = endSession; - return this; - } - - public Builder asExpirationUpdate() { - return asExpirationUpdate(true); - } - - public Builder asExpirationUpdate(boolean expirationUpdate) { - this.expirationUpdate = expirationUpdate; - return this; - } - - public Builder withExpiration(int expiresInSeconds) { - this.expiresInSeconds = expiresInSeconds; - return this; - } - - public Builder withProfileKey(byte[] profileKey) { - this.profileKey = profileKey; - return this; - } - - public Builder asProfileKeyUpdate(boolean profileKeyUpdate) { - this.profileKeyUpdate = profileKeyUpdate; - return this; - } - - public Builder withQuote(Quote quote) { - this.quote = quote; - return this; - } - - public Builder withSharedContact(SharedContact contact) { - this.sharedContacts.add(contact); - return this; - } - - public Builder withSharedContacts(List contacts) { - this.sharedContacts.addAll(contacts); - return this; - } - - public Builder withPreviews(List previews) { - this.previews.addAll(previews); - return this; - } - - public Builder withMentions(List mentions) { - this.mentions.addAll(mentions); - return this; - } - - public Builder withSticker(Sticker sticker) { - this.sticker = sticker; - return this; - } - - public Builder withViewOnce(boolean viewOnce) { - this.viewOnce = viewOnce; - return this; - } - - public Builder withReaction(Reaction reaction) { - this.reaction = reaction; - return this; - } - - public Builder withRemoteDelete(RemoteDelete remoteDelete) { - this.remoteDelete = remoteDelete; - return this; - } - - public Builder withGroupCallUpdate(GroupCallUpdate groupCallUpdate) { - this.groupCallUpdate = groupCallUpdate; - return this; - } - - public Builder withPayment(Payment payment) { - this.payment = payment; - return this; - } - - public Builder withStoryContext(StoryContext storyContext) { - this.storyContext = storyContext; - return this; - } - - public Builder withGiftBadge(GiftBadge giftBadge) { - this.giftBadge = giftBadge; - return this; - } - - public SignalServiceDataMessage build() { - if (timestamp == 0) timestamp = System.currentTimeMillis(); - return new SignalServiceDataMessage(timestamp, groupV2, attachments, body, endSession, - expiresInSeconds, expirationUpdate, profileKey, - profileKeyUpdate, quote, sharedContacts, previews, - mentions, sticker, viewOnce, reaction, remoteDelete, - groupCallUpdate, - payment, - storyContext, - giftBadge); - } - } - - public static class Quote { - private final long id; - private final ServiceId author; - private final String text; - private final List attachments; - private final List mentions; - private final Type type; - - public Quote(long id, - ServiceId author, - String text, - List attachments, - List mentions, - Type type) - { - this.id = id; - this.author = author; - this.text = text; - this.attachments = attachments; - this.mentions = mentions; - this.type = type; - } - - public long getId() { - return id; - } - - public ServiceId getAuthor() { - return author; - } - - public String getText() { - return text; - } - - public List getAttachments() { - return attachments; - } - - public List getMentions() { - return mentions; - } - - public Type getType() { - return type; - } - - public enum Type { - NORMAL(SignalServiceProtos.DataMessage.Quote.Type.NORMAL), - GIFT_BADGE(SignalServiceProtos.DataMessage.Quote.Type.GIFT_BADGE); - - private final SignalServiceProtos.DataMessage.Quote.Type protoType; - - Type(SignalServiceProtos.DataMessage.Quote.Type protoType) { - this.protoType = protoType; - } - - public SignalServiceProtos.DataMessage.Quote.Type getProtoType() { - return protoType; - } - - public static Type fromProto(SignalServiceProtos.DataMessage.Quote.Type protoType) { - for (final Type value : values()) { - if (value.protoType == protoType) { - return value; - } - } - - return NORMAL; - } - } - - public static class QuotedAttachment { - private final String contentType; - private final String fileName; - private final SignalServiceAttachment thumbnail; - - public QuotedAttachment(String contentType, String fileName, SignalServiceAttachment thumbnail) { - this.contentType = contentType; - this.fileName = fileName; - this.thumbnail = thumbnail; - } - - public String getContentType() { - return contentType; - } - - public String getFileName() { - return fileName; - } - - public SignalServiceAttachment getThumbnail() { - return thumbnail; - } - } - } - - public static class Sticker { - private final byte[] packId; - private final byte[] packKey; - private final int stickerId; - private final String emoji; - private final SignalServiceAttachment attachment; - - public Sticker(byte[] packId, byte[] packKey, int stickerId, String emoji, SignalServiceAttachment attachment) { - this.packId = packId; - this.packKey = packKey; - this.stickerId = stickerId; - this.emoji = emoji; - this.attachment = attachment; - } - - public byte[] getPackId() { - return packId; - } - - public byte[] getPackKey() { - return packKey; - } - - public int getStickerId() { - return stickerId; - } - - public String getEmoji() { - return emoji; - } - - public SignalServiceAttachment getAttachment() { - return attachment; - } - } - - public static class Reaction { - private final String emoji; - private final boolean remove; - private final ServiceId targetAuthor; - private final long targetSentTimestamp; - - public Reaction(String emoji, boolean remove, ServiceId targetAuthor, long targetSentTimestamp) { - this.emoji = emoji; - this.remove = remove; - this.targetAuthor = targetAuthor; - this.targetSentTimestamp = targetSentTimestamp; - } - - public String getEmoji() { - return emoji; - } - - public boolean isRemove() { - return remove; - } - - public ServiceId getTargetAuthor() { - return targetAuthor; - } - - public long getTargetSentTimestamp() { - return targetSentTimestamp; - } - } - - public static class RemoteDelete { - private final long targetSentTimestamp; - - public RemoteDelete(long targetSentTimestamp) { - this.targetSentTimestamp = targetSentTimestamp; - } - - public long getTargetSentTimestamp() { - return targetSentTimestamp; - } - } - - public static class Mention { - private final ServiceId serviceId; - private final int start; - private final int length; - - public Mention(ServiceId serviceId, int start, int length) { - this.serviceId = serviceId; - this.start = start; - this.length = length; - } - - public ServiceId getServiceId() { - return serviceId; - } - - public int getStart() { - return start; - } - - public int getLength() { - return length; - } - } - - public static class GroupCallUpdate { - private final String eraId; - - public GroupCallUpdate(String eraId) { - this.eraId = eraId; - } - - public String getEraId() { - return eraId; - } - } - - public static class PaymentNotification { - - private final byte[] receipt; - private final String note; - - public PaymentNotification(byte[] receipt, String note) { - this.receipt = receipt; - this.note = note; - } - - public byte[] getReceipt() { - return receipt; - } - - public String getNote() { - return note; - } - } - - public static class PaymentActivation { - private final SignalServiceProtos.DataMessage.Payment.Activation.Type type; - - public PaymentActivation(SignalServiceProtos.DataMessage.Payment.Activation.Type type) { - this.type = type; - } - - public SignalServiceProtos.DataMessage.Payment.Activation.Type getType() { - return type; - } - } - - public static class Payment { - private final Optional paymentNotification; - private final Optional paymentActivation; - - public Payment(PaymentNotification paymentNotification, PaymentActivation paymentActivation) { - this.paymentNotification = Optional.ofNullable(paymentNotification); - this.paymentActivation = Optional.ofNullable(paymentActivation); - } - - public Optional getPaymentNotification() { - return paymentNotification; - } - - public Optional getPaymentActivation() { - return paymentActivation; - } - } - - public static class StoryContext { - private final ServiceId authorServiceId; - private final long sentTimestamp; - - public StoryContext(ServiceId authorServiceId, long sentTimestamp) { - this.authorServiceId = authorServiceId; - this.sentTimestamp = sentTimestamp; - } - - public ServiceId getAuthorServiceId() { - return authorServiceId; - } - - public long getSentTimestamp() { - return sentTimestamp; - } - } - - public static class GiftBadge { - private final ReceiptCredentialPresentation receiptCredentialPresentation; - - public GiftBadge(ReceiptCredentialPresentation receiptCredentialPresentation) { - this.receiptCredentialPresentation = receiptCredentialPresentation; - } - - public ReceiptCredentialPresentation getReceiptCredentialPresentation() { - return receiptCredentialPresentation; - } - } -} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.kt new file mode 100644 index 000000000..03d287f39 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.kt @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2014-2016 Open Whisper Systems + * + * Licensed according to the LICENSE file in this repository. + */ +package org.whispersystems.signalservice.api.messages + +import org.signal.libsignal.zkgroup.groups.GroupSecretParams +import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation +import org.whispersystems.signalservice.api.messages.shared.SharedContact +import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.api.util.OptionalUtil.asOptional +import org.whispersystems.signalservice.api.util.OptionalUtil.emptyIfStringEmpty +import java.util.LinkedList +import java.util.Optional +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage.Payment as PaymentProto +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage.Quote as QuoteProto + +/** + * Represents a decrypted Signal Service data message. + * + * @param timestamp The sent timestamp. + * @param groupContext The group information (or null if none). + * @param attachments The attachments (or null if none). + * @param body The message contents. + * @param isEndSession Flag indicating whether this message should close a session. + * @param expiresInSeconds Number of seconds in which the message should disappear after being seen. + */ +class SignalServiceDataMessage private constructor( + val timestamp: Long, + val groupContext: Optional, + val attachments: Optional>, + val body: Optional, + val isEndSession: Boolean, + val expiresInSeconds: Int, + val isExpirationUpdate: Boolean, + val profileKey: Optional, + val isProfileKeyUpdate: Boolean, + val quote: Optional, + val sharedContacts: Optional>, + val previews: Optional>, + val mentions: Optional>, + val sticker: Optional, + val isViewOnce: Boolean, + val reaction: Optional, + val remoteDelete: Optional, + val groupCallUpdate: Optional, + val payment: Optional, + val storyContext: Optional, + val giftBadge: Optional +) { + val isActivatePaymentsRequest: Boolean = payment.map { it.isActivationRequest }.orElse(false) + val isPaymentsActivated: Boolean = payment.map { it.isActivation }.orElse(false) + + val groupId: Optional = groupContext.map { GroupSecretParams.deriveFromMasterKey(it.masterKey).publicParams.groupIdentifier.serialize() } + val isGroupV2Message: Boolean = groupContext.isPresent + + /** Contains some user data that affects the conversation */ + private val hasRenderableContent: Boolean = + this.attachments.isPresent || + this.body.isPresent || + this.quote.isPresent || + this.sharedContacts.isPresent || + this.previews.isPresent || + this.mentions.isPresent || + this.sticker.isPresent || + this.reaction.isPresent || + this.remoteDelete.isPresent + + val isGroupV2Update: Boolean = groupContext.isPresent && groupContext.get().hasSignedGroupChange() && !hasRenderableContent + val isEmptyGroupV2Message: Boolean = isGroupV2Message && !isGroupV2Update && !hasRenderableContent + + class Builder { + private var timestamp: Long = 0 + private var groupV2: SignalServiceGroupV2? = null + private val attachments: MutableList = LinkedList() + private var body: String? = null + private var endSession: Boolean = false + private var expiresInSeconds: Int = 0 + private var expirationUpdate: Boolean = false + private var profileKey: ByteArray? = null + private var profileKeyUpdate: Boolean = false + private var quote: Quote? = null + private val sharedContacts: MutableList = LinkedList() + private val previews: MutableList = LinkedList() + private val mentions: MutableList = LinkedList() + private var sticker: Sticker? = null + private var viewOnce: Boolean = false + private var reaction: Reaction? = null + private var remoteDelete: RemoteDelete? = null + private var groupCallUpdate: GroupCallUpdate? = null + private var payment: Payment? = null + private var storyContext: StoryContext? = null + private var giftBadge: GiftBadge? = null + + fun withTimestamp(timestamp: Long): Builder { + this.timestamp = timestamp + return this + } + + fun asGroupMessage(group: SignalServiceGroupV2?): Builder { + groupV2 = group + return this + } + + fun withAttachment(attachment: SignalServiceAttachment?): Builder { + attachment?.let { attachments.add(attachment) } + return this + } + + fun withAttachments(attachments: List?): Builder { + attachments?.let { this.attachments.addAll(attachments) } + return this + } + + fun withBody(body: String?): Builder { + this.body = body + return this + } + + @JvmOverloads + fun asEndSessionMessage(endSession: Boolean = true): Builder { + this.endSession = endSession + return this + } + + @JvmOverloads + fun asExpirationUpdate(expirationUpdate: Boolean = true): Builder { + this.expirationUpdate = expirationUpdate + return this + } + + fun withExpiration(expiresInSeconds: Int): Builder { + this.expiresInSeconds = expiresInSeconds + return this + } + + fun withProfileKey(profileKey: ByteArray?): Builder { + this.profileKey = profileKey + return this + } + + fun asProfileKeyUpdate(profileKeyUpdate: Boolean): Builder { + this.profileKeyUpdate = profileKeyUpdate + return this + } + + fun withQuote(quote: Quote?): Builder { + this.quote = quote + return this + } + + fun withSharedContact(contact: SharedContact?): Builder { + contact?.let { sharedContacts.add(contact) } + return this + } + + fun withSharedContacts(contacts: List?): Builder { + contacts?.let { sharedContacts.addAll(contacts) } + return this + } + + fun withPreviews(previews: List?): Builder { + previews?.let { this.previews.addAll(previews) } + return this + } + + fun withMentions(mentions: List?): Builder { + mentions?.let { this.mentions.addAll(mentions) } + return this + } + + fun withSticker(sticker: Sticker?): Builder { + this.sticker = sticker + return this + } + + fun withViewOnce(viewOnce: Boolean): Builder { + this.viewOnce = viewOnce + return this + } + + fun withReaction(reaction: Reaction?): Builder { + this.reaction = reaction + return this + } + + fun withRemoteDelete(remoteDelete: RemoteDelete?): Builder { + this.remoteDelete = remoteDelete + return this + } + + fun withGroupCallUpdate(groupCallUpdate: GroupCallUpdate?): Builder { + this.groupCallUpdate = groupCallUpdate + return this + } + + fun withPayment(payment: Payment?): Builder { + this.payment = payment + return this + } + + fun withStoryContext(storyContext: StoryContext?): Builder { + this.storyContext = storyContext + return this + } + + fun withGiftBadge(giftBadge: GiftBadge?): Builder { + this.giftBadge = giftBadge + return this + } + + fun build(): SignalServiceDataMessage { + if (timestamp == 0L) { + timestamp = System.currentTimeMillis() + } + + return SignalServiceDataMessage( + timestamp = timestamp, + groupContext = groupV2.asOptional(), + attachments = attachments.asOptional(), + body = body.emptyIfStringEmpty(), + isEndSession = endSession, + expiresInSeconds = expiresInSeconds, + isExpirationUpdate = expirationUpdate, + profileKey = profileKey.asOptional(), + isProfileKeyUpdate = profileKeyUpdate, + quote = quote.asOptional(), + sharedContacts = sharedContacts.asOptional(), + previews = previews.asOptional(), + mentions = mentions.asOptional(), + sticker = sticker.asOptional(), + isViewOnce = viewOnce, + reaction = reaction.asOptional(), + remoteDelete = remoteDelete.asOptional(), + groupCallUpdate = groupCallUpdate.asOptional(), + payment = payment.asOptional(), + storyContext = storyContext.asOptional(), + giftBadge = giftBadge.asOptional() + ) + } + } + + data class Quote( + val id: Long, + val author: ServiceId?, + val text: String, + val attachments: List?, + val mentions: List?, + val type: Type + ) { + enum class Type(val protoType: QuoteProto.Type) { + NORMAL(QuoteProto.Type.NORMAL), + GIFT_BADGE(QuoteProto.Type.GIFT_BADGE); + + companion object { + @JvmStatic + fun fromProto(protoType: QuoteProto.Type): Type { + return values().firstOrNull { it.protoType == protoType } ?: NORMAL + } + } + } + + data class QuotedAttachment(val contentType: String, val fileName: String?, val thumbnail: SignalServiceAttachment?) + } + class Sticker(val packId: ByteArray?, val packKey: ByteArray?, val stickerId: Int, val emoji: String?, val attachment: SignalServiceAttachment?) + data class Reaction(val emoji: String, val isRemove: Boolean, val targetAuthor: ServiceId, val targetSentTimestamp: Long) + data class RemoteDelete(val targetSentTimestamp: Long) + data class Mention(val serviceId: ServiceId, val start: Int, val length: Int) + data class GroupCallUpdate(val eraId: String?) + class PaymentNotification(val receipt: ByteArray, val note: String) + data class PaymentActivation(val type: PaymentProto.Activation.Type) + class Payment(paymentNotification: PaymentNotification?, paymentActivation: PaymentActivation?) { + val paymentNotification: Optional = Optional.ofNullable(paymentNotification) + val paymentActivation: Optional = Optional.ofNullable(paymentActivation) + val isActivationRequest: Boolean = paymentActivation != null && paymentActivation.type == PaymentProto.Activation.Type.REQUEST + val isActivation: Boolean = paymentActivation != null && paymentActivation.type == PaymentProto.Activation.Type.ACTIVATED + } + data class StoryContext(val authorServiceId: ServiceId, val sentTimestamp: Long) + data class GiftBadge(val receiptCredentialPresentation: ReceiptCredentialPresentation) + + companion object { + @JvmStatic + fun newBuilder(): Builder { + return Builder() + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.java deleted file mode 100644 index 52b56ae0e..000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.whispersystems.signalservice.api.util; - -import com.google.protobuf.ByteString; - -import java.util.Arrays; -import java.util.Optional; - -public final class OptionalUtil { - - private OptionalUtil() { } - - @SafeVarargs - public static Optional or(Optional... optionals) { - return Arrays.stream(optionals) - .filter(Optional::isPresent) - .findFirst() - .orElse(Optional.empty()); - } - - public static boolean byteArrayEquals(Optional a, Optional b) { - if (a.isPresent() != b.isPresent()) { - return false; - } else if (a.isPresent()) { - return Arrays.equals(a.get(), b.get()); - } else { - return true; - } - } - - public static int byteArrayHashCode(Optional bytes) { - if (bytes.isPresent()) { - return Arrays.hashCode(bytes.get()); - } else { - return 0; - } - } - - public static Optional absentIfEmpty(String value) { - if (value == null || value.length() == 0) { - return Optional.empty(); - } else { - return Optional.of(value); - } - } - - public static Optional absentIfEmpty(ByteString value) { - if (value == null || value.isEmpty()) { - return Optional.empty(); - } else { - return Optional.of(value.toByteArray()); - } - } -} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.kt new file mode 100644 index 000000000..469d2419f --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.kt @@ -0,0 +1,67 @@ +package org.whispersystems.signalservice.api.util + +import com.google.protobuf.ByteString +import java.util.Optional + +object OptionalUtil { + @JvmStatic + @SafeVarargs + fun or(vararg optionals: Optional): Optional { + return optionals.firstOrNull { it.isPresent } ?: Optional.empty() + } + + @JvmStatic + fun byteArrayEquals(a: Optional, b: Optional): Boolean { + return if (a.isPresent != b.isPresent) { + false + } else if (a.isPresent) { + a.get().contentEquals(b.get()) + } else { + true + } + } + + @JvmStatic + fun byteArrayHashCode(bytes: Optional): Int { + return if (bytes.isPresent) { + bytes.get().contentHashCode() + } else { + 0 + } + } + + @JvmStatic + fun absentIfEmpty(value: String?): Optional { + return if (value == null || value.isEmpty()) { + Optional.empty() + } else { + Optional.of(value) + } + } + + @JvmStatic + fun absentIfEmpty(value: ByteString?): Optional { + return if (value == null || value.isEmpty) { + Optional.empty() + } else { + Optional.of(value.toByteArray()) + } + } + + @JvmStatic + fun emptyIfListEmpty(list: List?): Optional> { + return list.asOptional() + } + + fun E?.asOptional(): Optional { + return Optional.ofNullable(this) + } + + fun List?.asOptional(): Optional> { + return Optional.ofNullable(this?.takeIf { it.isNotEmpty() }) + } + + fun String?.emptyIfStringEmpty(): Optional { + return absentIfEmpty(this) + } +}