Signal-Android/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java

520 wiersze
16 KiB
Java

package org.whispersystems.signalservice.api.storage;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.payments.PaymentsConstants;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import org.whispersystems.signalservice.api.util.ProtoUtil;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
public final class SignalAccountRecord implements SignalRecord {
private static final String TAG = SignalAccountRecord.class.getSimpleName();
private final StorageId id;
private final AccountRecord proto;
private final boolean hasUnknownFields;
private final Optional<String> givenName;
private final Optional<String> familyName;
private final Optional<String> avatarUrlPath;
private final Optional<byte[]> profileKey;
private final List<PinnedConversation> pinnedConversations;
private final Payments payments;
public SignalAccountRecord(StorageId id, AccountRecord proto) {
this.id = id;
this.proto = proto;
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
this.givenName = OptionalUtil.absentIfEmpty(proto.getGivenName());
this.familyName = OptionalUtil.absentIfEmpty(proto.getFamilyName());
this.profileKey = OptionalUtil.absentIfEmpty(proto.getProfileKey());
this.avatarUrlPath = OptionalUtil.absentIfEmpty(proto.getAvatarUrlPath());
this.pinnedConversations = new ArrayList<>(proto.getPinnedConversationsCount());
this.payments = new Payments(proto.getPayments().getEnabled(), OptionalUtil.absentIfEmpty(proto.getPayments().getEntropy()));
for (AccountRecord.PinnedConversation conversation : proto.getPinnedConversationsList()) {
pinnedConversations.add(PinnedConversation.fromRemote(conversation));
}
}
@Override
public StorageId getId() {
return id;
}
@Override
public SignalStorageRecord asStorageRecord() {
return SignalStorageRecord.forAccount(this);
}
@Override
public String describeDiff(SignalRecord other) {
if (other instanceof SignalAccountRecord) {
SignalAccountRecord that = (SignalAccountRecord) other;
List<String> diff = new LinkedList<>();
if (!Arrays.equals(this.id.getRaw(), that.id.getRaw())) {
diff.add("ID");
}
if (!Objects.equals(this.givenName, that.givenName)) {
diff.add("GivenName");
}
if (!Objects.equals(this.familyName, that.familyName)) {
diff.add("FamilyName");
}
if (!OptionalUtil.byteArrayEquals(this.profileKey, that.profileKey)) {
diff.add("ProfileKey");
}
if (!Objects.equals(this.avatarUrlPath, that.avatarUrlPath)) {
diff.add("AvatarUrlPath");
}
if (!Objects.equals(this.isNoteToSelfArchived(), that.isNoteToSelfArchived())) {
diff.add("NoteToSelfArchived");
}
if (!Objects.equals(this.isNoteToSelfForcedUnread(), that.isNoteToSelfForcedUnread())) {
diff.add("NoteToSelfForcedUnread");
}
if (!Objects.equals(this.isReadReceiptsEnabled(), that.isReadReceiptsEnabled())) {
diff.add("ReadReceipts");
}
if (!Objects.equals(this.isTypingIndicatorsEnabled(), that.isTypingIndicatorsEnabled())) {
diff.add("TypingIndicators");
}
if (!Objects.equals(this.isSealedSenderIndicatorsEnabled(), that.isSealedSenderIndicatorsEnabled())) {
diff.add("SealedSenderIndicators");
}
if (!Objects.equals(this.isLinkPreviewsEnabled(), that.isLinkPreviewsEnabled())) {
diff.add("LinkPreviews");
}
if (!Objects.equals(this.getPhoneNumberSharingMode(), that.getPhoneNumberSharingMode())) {
diff.add("PhoneNumberSharingMode");
}
if (!Objects.equals(this.isPhoneNumberUnlisted(), that.isPhoneNumberUnlisted())) {
diff.add("PhoneNumberUnlisted");
}
if (!Objects.equals(this.pinnedConversations, that.pinnedConversations)) {
diff.add("PinnedConversations");
}
if (!Objects.equals(this.isPreferContactAvatars(), that.isPreferContactAvatars())) {
diff.add("PreferContactAvatars");
}
if (!Objects.equals(this.payments, that.payments)) {
diff.add("Payments");
}
if (this.getUniversalExpireTimer() != that.getUniversalExpireTimer()) {
diff.add("UniversalExpireTimer");
}
if (!Objects.equals(this.isPrimarySendsSms(), that.isPrimarySendsSms())) {
diff.add("PrimarySendsSms");
}
if (!Objects.equals(this.getE164(), that.getE164())) {
diff.add("E164");
}
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
diff.add("UnknownFields");
}
return diff.toString();
} else {
return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName();
}
}
public boolean hasUnknownFields() {
return hasUnknownFields;
}
public byte[] serializeUnknownFields() {
return hasUnknownFields ? proto.toByteArray() : null;
}
public Optional<String> getGivenName() {
return givenName;
}
public Optional<String> getFamilyName() {
return familyName;
}
public Optional<byte[]> getProfileKey() {
return profileKey;
}
public Optional<String> getAvatarUrlPath() {
return avatarUrlPath;
}
public boolean isNoteToSelfArchived() {
return proto.getNoteToSelfArchived();
}
public boolean isNoteToSelfForcedUnread() {
return proto.getNoteToSelfMarkedUnread();
}
public boolean isReadReceiptsEnabled() {
return proto.getReadReceipts();
}
public boolean isTypingIndicatorsEnabled() {
return proto.getTypingIndicators();
}
public boolean isSealedSenderIndicatorsEnabled() {
return proto.getSealedSenderIndicators();
}
public boolean isLinkPreviewsEnabled() {
return proto.getLinkPreviews();
}
public AccountRecord.PhoneNumberSharingMode getPhoneNumberSharingMode() {
return proto.getPhoneNumberSharingMode();
}
public boolean isPhoneNumberUnlisted() {
return proto.getUnlistedPhoneNumber();
}
public List<PinnedConversation> getPinnedConversations() {
return pinnedConversations;
}
public boolean isPreferContactAvatars() {
return proto.getPreferContactAvatars();
}
public Payments getPayments() {
return payments;
}
public int getUniversalExpireTimer() {
return proto.getUniversalExpireTimer();
}
public boolean isPrimarySendsSms() {
return proto.getPrimarySendsSms();
}
public String getE164() {
return proto.getE164();
}
AccountRecord toProto() {
return proto;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SignalAccountRecord that = (SignalAccountRecord) o;
return id.equals(that.id) &&
proto.equals(that.proto);
}
@Override
public int hashCode() {
return Objects.hash(id, proto);
}
public static class PinnedConversation {
private final Optional<SignalServiceAddress> contact;
private final Optional<byte[]> groupV1Id;
private final Optional<byte[]> groupV2MasterKey;
private PinnedConversation(Optional<SignalServiceAddress> contact, Optional<byte[]> groupV1Id, Optional<byte[]> groupV2MasterKey) {
this.contact = contact;
this.groupV1Id = groupV1Id;
this.groupV2MasterKey = groupV2MasterKey;
}
public static PinnedConversation forContact(SignalServiceAddress address) {
return new PinnedConversation(Optional.of(address), Optional.absent(), Optional.absent());
}
public static PinnedConversation forGroupV1(byte[] groupId) {
return new PinnedConversation(Optional.absent(), Optional.of(groupId), Optional.absent());
}
public static PinnedConversation forGroupV2(byte[] masterKey) {
return new PinnedConversation(Optional.absent(), Optional.absent(), Optional.of(masterKey));
}
private static PinnedConversation forEmpty() {
return new PinnedConversation(Optional.absent(), Optional.absent(), Optional.absent());
}
static PinnedConversation fromRemote(AccountRecord.PinnedConversation remote) {
if (remote.hasContact()) {
return forContact(new SignalServiceAddress(UuidUtil.parseOrThrow(remote.getContact().getUuid()), remote.getContact().getE164()));
} else if (!remote.getLegacyGroupId().isEmpty()) {
return forGroupV1(remote.getLegacyGroupId().toByteArray());
} else if (!remote.getGroupMasterKey().isEmpty()) {
return forGroupV2(remote.getGroupMasterKey().toByteArray());
} else {
return PinnedConversation.forEmpty();
}
}
public Optional<SignalServiceAddress> getContact() {
return contact;
}
public Optional<byte[]> getGroupV1Id() {
return groupV1Id;
}
public Optional<byte[]> getGroupV2MasterKey() {
return groupV2MasterKey;
}
public boolean isValid() {
return contact.isPresent() || groupV1Id.isPresent() || groupV2MasterKey.isPresent();
}
private AccountRecord.PinnedConversation toRemote() {
if (contact.isPresent()) {
AccountRecord.PinnedConversation.Contact.Builder contactBuilder = AccountRecord.PinnedConversation.Contact.newBuilder();
contactBuilder.setUuid(contact.get().getUuid().toString());
if (contact.get().getNumber().isPresent()) {
contactBuilder.setE164(contact.get().getNumber().get());
}
return AccountRecord.PinnedConversation.newBuilder().setContact(contactBuilder.build()).build();
} else if (groupV1Id.isPresent()) {
return AccountRecord.PinnedConversation.newBuilder().setLegacyGroupId(ByteString.copyFrom(groupV1Id.get())).build();
} else if (groupV2MasterKey.isPresent()) {
return AccountRecord.PinnedConversation.newBuilder().setGroupMasterKey(ByteString.copyFrom(groupV2MasterKey.get())).build();
} else {
return AccountRecord.PinnedConversation.newBuilder().build();
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PinnedConversation that = (PinnedConversation) o;
return contact.equals(that.contact) &&
groupV1Id.equals(that.groupV1Id) &&
groupV2MasterKey.equals(that.groupV2MasterKey);
}
@Override
public int hashCode() {
return Objects.hash(contact, groupV1Id, groupV2MasterKey);
}
}
public static class Payments {
private static final String TAG = Payments.class.getSimpleName();
private final boolean enabled;
private final Optional<byte[]> entropy;
public Payments(boolean enabled, Optional<byte[]> entropy) {
byte[] entropyBytes = entropy.orNull();
if (entropyBytes != null && entropyBytes.length != PaymentsConstants.PAYMENTS_ENTROPY_LENGTH) {
Log.w(TAG, "Blocked entropy of length " + entropyBytes.length);
entropyBytes = null;
}
this.entropy = Optional.fromNullable(entropyBytes);
this.enabled = enabled && this.entropy.isPresent();
}
public boolean isEnabled() {
return enabled;
}
public Optional<byte[]> getEntropy() {
return entropy;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Payments payments = (Payments) o;
return enabled == payments.enabled &&
OptionalUtil.byteArrayEquals(entropy, payments.entropy);
}
@Override
public int hashCode() {
return Objects.hash(enabled, entropy);
}
}
public static final class Builder {
private final StorageId id;
private final AccountRecord.Builder builder;
private byte[] unknownFields;
public Builder(byte[] rawId) {
this.id = StorageId.forAccount(rawId);
this.builder = AccountRecord.newBuilder();
}
public Builder setUnknownFields(byte[] serializedUnknowns) {
this.unknownFields = serializedUnknowns;
return this;
}
public Builder setGivenName(String givenName) {
builder.setGivenName(givenName == null ? "" : givenName);
return this;
}
public Builder setFamilyName(String familyName) {
builder.setFamilyName(familyName == null ? "" : familyName);
return this;
}
public Builder setProfileKey(byte[] profileKey) {
builder.setProfileKey(profileKey == null ? ByteString.EMPTY : ByteString.copyFrom(profileKey));
return this;
}
public Builder setAvatarUrlPath(String urlPath) {
builder.setAvatarUrlPath(urlPath == null ? "" : urlPath);
return this;
}
public Builder setNoteToSelfArchived(boolean archived) {
builder.setNoteToSelfArchived(archived);
return this;
}
public Builder setNoteToSelfForcedUnread(boolean forcedUnread) {
builder.setNoteToSelfMarkedUnread(forcedUnread);
return this;
}
public Builder setReadReceiptsEnabled(boolean enabled) {
builder.setReadReceipts(enabled);
return this;
}
public Builder setTypingIndicatorsEnabled(boolean enabled) {
builder.setTypingIndicators(enabled);
return this;
}
public Builder setSealedSenderIndicatorsEnabled(boolean enabled) {
builder.setSealedSenderIndicators(enabled);
return this;
}
public Builder setLinkPreviewsEnabled(boolean enabled) {
builder.setLinkPreviews(enabled);
return this;
}
public Builder setPhoneNumberSharingMode(AccountRecord.PhoneNumberSharingMode mode) {
builder.setPhoneNumberSharingMode(mode);
return this;
}
public Builder setUnlistedPhoneNumber(boolean unlisted) {
builder.setUnlistedPhoneNumber(unlisted);
return this;
}
public Builder setPinnedConversations(List<PinnedConversation> pinnedConversations) {
builder.clearPinnedConversations();
for (PinnedConversation pinned : pinnedConversations) {
builder.addPinnedConversations(pinned.toRemote());
}
return this;
}
public Builder setPreferContactAvatars(boolean preferContactAvatars) {
builder.setPreferContactAvatars(preferContactAvatars);
return this;
}
public Builder setPayments(boolean enabled, byte[] entropy) {
org.whispersystems.signalservice.internal.storage.protos.Payments.Builder paymentsBuilder = org.whispersystems.signalservice.internal.storage.protos.Payments.newBuilder();
boolean entropyPresent = entropy != null && entropy.length == PaymentsConstants.PAYMENTS_ENTROPY_LENGTH;
paymentsBuilder.setEnabled(enabled && entropyPresent);
if (entropyPresent) {
paymentsBuilder.setEntropy(ByteString.copyFrom(entropy));
}
builder.setPayments(paymentsBuilder);
return this;
}
public Builder setUniversalExpireTimer(int timer) {
builder.setUniversalExpireTimer(timer);
return this;
}
public Builder setPrimarySendsSms(boolean primarySendsSms) {
builder.setPrimarySendsSms(primarySendsSms);
return this;
}
public Builder setE164(String e164) {
builder.setE164(e164);
return this;
}
public SignalAccountRecord build() {
AccountRecord proto = builder.build();
if (unknownFields != null) {
try {
proto = ProtoUtil.combineWithUnknownFields(proto, unknownFields);
} catch (InvalidProtocolBufferException e) {
Log.w(TAG, "Failed to combine unknown fields!", e);
throw new IllegalStateException(e);
}
}
return new SignalAccountRecord(id, proto);
}
}
}