kopia lustrzana https://github.com/ryukoposting/Signal-Android
520 wiersze
16 KiB
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);
|
|
}
|
|
}
|
|
}
|