Add support for syncing pinned status with storage service.
This commit is contained in:
parent
97420aae1b
commit
7ef57cc0cf
8 changed files with 295 additions and 49 deletions
|
@ -32,6 +32,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
|
||||||
import org.jsoup.helper.StringUtil;
|
import org.jsoup.helper.StringUtil;
|
||||||
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
|
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
@ -39,6 +41,8 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||||
|
import org.thoughtcrime.securesms.groups.BadGroupIdException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
|
@ -758,6 +762,25 @@ public class ThreadDatabase extends Database {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Pinned recipients, in order from top to bottom.
|
||||||
|
*/
|
||||||
|
public @NonNull List<RecipientId> getPinnedRecipientIds() {
|
||||||
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
|
String[] projection = new String[]{RECIPIENT_ID};
|
||||||
|
String query = PINNED + " > ?";
|
||||||
|
String[] args = SqlUtil.buildArgs(0);
|
||||||
|
List<RecipientId> pinned = new LinkedList<>();
|
||||||
|
|
||||||
|
try (Cursor cursor = db.query(TABLE_NAME, projection, query, args, null, null, PINNED + " ASC")) {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
pinned.add(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pinned;
|
||||||
|
}
|
||||||
|
|
||||||
public void pinConversations(@NonNull Set<Long> threadIds) {
|
public void pinConversations(@NonNull Set<Long> threadIds) {
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
|
||||||
|
@ -771,6 +794,7 @@ public class ThreadDatabase extends Database {
|
||||||
contentValues.put(PINNED, ++pinnedCount);
|
contentValues.put(PINNED, ++pinnedCount);
|
||||||
|
|
||||||
db.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(threadId));
|
db.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(threadId));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
|
@ -780,6 +804,9 @@ public class ThreadDatabase extends Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyConversationListListeners();
|
notifyConversationListListeners();
|
||||||
|
|
||||||
|
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().getId());
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unpinConversations(@NonNull Set<Long> threadIds) {
|
public void unpinConversations(@NonNull Set<Long> threadIds) {
|
||||||
|
@ -792,6 +819,9 @@ public class ThreadDatabase extends Database {
|
||||||
|
|
||||||
db.update(TABLE_NAME, contentValues, selection, SqlUtil.buildArgs(Stream.of(threadIds).toArray()));
|
db.update(TABLE_NAME, contentValues, selection, SqlUtil.buildArgs(Stream.of(threadIds).toArray()));
|
||||||
notifyConversationListListeners();
|
notifyConversationListListeners();
|
||||||
|
|
||||||
|
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().getId());
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void archiveConversation(long threadId) {
|
public void archiveConversation(long threadId) {
|
||||||
|
@ -1025,7 +1055,57 @@ public class ThreadDatabase extends Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalAccountRecord record) {
|
public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalAccountRecord record) {
|
||||||
applyStorageSyncUpdate(recipientId, record.isNoteToSelfArchived(), record.isNoteToSelfForcedUnread());
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
|
||||||
|
db.beginTransaction();
|
||||||
|
try {
|
||||||
|
applyStorageSyncUpdate(recipientId, record.isNoteToSelfArchived(), record.isNoteToSelfForcedUnread());
|
||||||
|
|
||||||
|
ContentValues clearPinnedValues = new ContentValues();
|
||||||
|
clearPinnedValues.put(PINNED, 0);
|
||||||
|
db.update(TABLE_NAME, clearPinnedValues, null, null);
|
||||||
|
|
||||||
|
int pinnedPosition = 1;
|
||||||
|
for (SignalAccountRecord.PinnedConversation pinned : record.getPinnedConversations()) {
|
||||||
|
ContentValues pinnedValues = new ContentValues();
|
||||||
|
pinnedValues.put(PINNED, pinnedPosition);
|
||||||
|
|
||||||
|
Recipient pinnedRecipient;
|
||||||
|
|
||||||
|
if (pinned.getContact().isPresent()) {
|
||||||
|
pinnedRecipient = Recipient.externalPush(context, pinned.getContact().get());
|
||||||
|
} else if (pinned.getGroupV1Id().isPresent()) {
|
||||||
|
try {
|
||||||
|
pinnedRecipient = Recipient.externalGroup(context, GroupId.v1Exact(pinned.getGroupV1Id().get()));
|
||||||
|
} catch (BadGroupIdException e) {
|
||||||
|
Log.w(TAG, "Failed to parse pinned groupV1 ID!", e);
|
||||||
|
pinnedRecipient = null;
|
||||||
|
}
|
||||||
|
} else if (pinned.getGroupV2MasterKey().isPresent()) {
|
||||||
|
try {
|
||||||
|
pinnedRecipient = Recipient.externalGroup(context, GroupId.v2(new GroupMasterKey(pinned.getGroupV2MasterKey().get())));
|
||||||
|
} catch (InvalidInputException e) {
|
||||||
|
Log.w(TAG, "Failed to parse pinned groupV2 master key!", e);
|
||||||
|
pinnedRecipient = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Empty pinned conversation on the AccountRecord?");
|
||||||
|
pinnedRecipient = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pinnedRecipient != null) {
|
||||||
|
db.update(TABLE_NAME, pinnedValues, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(pinnedRecipient.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
pinnedPosition++;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
} finally {
|
||||||
|
db.endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyConversationListListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyStorageSyncUpdate(@NonNull RecipientId recipientId, boolean archived, boolean forcedUnread) {
|
private void applyStorageSyncUpdate(@NonNull RecipientId recipientId, boolean archived, boolean forcedUnread) {
|
||||||
|
|
|
@ -88,7 +88,7 @@ final class MessageRequestRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
return MessageRequestState.REQUIRED;
|
return MessageRequestState.REQUIRED;
|
||||||
} else if (FeatureFlags.modernProfileSharing() && !recipient.isPushV2Group() && !recipient.isProfileSharing()) {
|
} else if (FeatureFlags.modernProfileSharing() && !RecipientUtil.isLegacyProfileSharingAccepted(recipient)) {
|
||||||
return MessageRequestState.REQUIRED;
|
return MessageRequestState.REQUIRED;
|
||||||
} else if (RecipientUtil.isPreMessageRequestThread(context, threadId) && !RecipientUtil.isLegacyProfileSharingAccepted(recipient)) {
|
} else if (RecipientUtil.isPreMessageRequestThread(context, threadId) && !RecipientUtil.isLegacyProfileSharingAccepted(recipient)) {
|
||||||
return MessageRequestState.PRE_MESSAGE_REQUEST;
|
return MessageRequestState.PRE_MESSAGE_REQUEST;
|
||||||
|
|
|
@ -256,7 +256,8 @@ public class RecipientUtil {
|
||||||
return threadRecipient.isLocalNumber() ||
|
return threadRecipient.isLocalNumber() ||
|
||||||
threadRecipient.isProfileSharing() ||
|
threadRecipient.isProfileSharing() ||
|
||||||
threadRecipient.isSystemContact() ||
|
threadRecipient.isSystemContact() ||
|
||||||
!threadRecipient.isRegistered();
|
!threadRecipient.isRegistered() ||
|
||||||
|
threadRecipient.isForceSmsSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
|
|
|
@ -6,11 +6,13 @@ import androidx.annotation.Nullable;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||||
|
import org.whispersystems.signalservice.api.storage.SignalAccountRecord.PinnedConversation;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -66,9 +68,10 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAc
|
||||||
boolean sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled();
|
boolean sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled();
|
||||||
boolean linkPreviews = remote.isLinkPreviewsEnabled();
|
boolean linkPreviews = remote.isLinkPreviewsEnabled();
|
||||||
boolean unlisted = remote.isPhoneNumberUnlisted();
|
boolean unlisted = remote.isPhoneNumberUnlisted();
|
||||||
|
List<PinnedConversation> pinnedConversations = remote.getPinnedConversations();
|
||||||
AccountRecord.PhoneNumberSharingMode phoneNumberSharingMode = remote.getPhoneNumberSharingMode();
|
AccountRecord.PhoneNumberSharingMode phoneNumberSharingMode = remote.getPhoneNumberSharingMode();
|
||||||
boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted);
|
boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations);
|
||||||
boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted );
|
boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations);
|
||||||
|
|
||||||
if (matchesRemote) {
|
if (matchesRemote) {
|
||||||
return remote;
|
return remote;
|
||||||
|
@ -90,6 +93,7 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAc
|
||||||
.setUnlistedPhoneNumber(unlisted)
|
.setUnlistedPhoneNumber(unlisted)
|
||||||
.setPhoneNumberSharingMode(phoneNumberSharingMode)
|
.setPhoneNumberSharingMode(phoneNumberSharingMode)
|
||||||
.setUnlistedPhoneNumber(unlisted)
|
.setUnlistedPhoneNumber(unlisted)
|
||||||
|
.setPinnedConversations(pinnedConversations)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,7 +111,8 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAc
|
||||||
boolean sealedSenderIndicators,
|
boolean sealedSenderIndicators,
|
||||||
boolean linkPreviewsEnabled,
|
boolean linkPreviewsEnabled,
|
||||||
AccountRecord.PhoneNumberSharingMode phoneNumberSharingMode,
|
AccountRecord.PhoneNumberSharingMode phoneNumberSharingMode,
|
||||||
boolean unlistedPhoneNumber)
|
boolean unlistedPhoneNumber,
|
||||||
|
@NonNull List<PinnedConversation> pinnedConversations)
|
||||||
{
|
{
|
||||||
return Arrays.equals(contact.serializeUnknownFields(), unknownFields) &&
|
return Arrays.equals(contact.serializeUnknownFields(), unknownFields) &&
|
||||||
Objects.equals(contact.getGivenName().or(""), givenName) &&
|
Objects.equals(contact.getGivenName().or(""), givenName) &&
|
||||||
|
@ -121,6 +126,7 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAc
|
||||||
contact.isSealedSenderIndicatorsEnabled() == sealedSenderIndicators &&
|
contact.isSealedSenderIndicatorsEnabled() == sealedSenderIndicators &&
|
||||||
contact.isLinkPreviewsEnabled() == linkPreviewsEnabled &&
|
contact.isLinkPreviewsEnabled() == linkPreviewsEnabled &&
|
||||||
contact.getPhoneNumberSharingMode() == phoneNumberSharingMode &&
|
contact.getPhoneNumberSharingMode() == phoneNumberSharingMode &&
|
||||||
contact.isPhoneNumberUnlisted() == unlistedPhoneNumber;
|
contact.isPhoneNumberUnlisted() == unlistedPhoneNumber &&
|
||||||
|
Objects.equals(contact.getPinnedConversations(), pinnedConversations);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -412,7 +412,11 @@ public final class StorageSyncHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord buildAccountRecord(@NonNull Context context, @NonNull Recipient self) {
|
public static SignalStorageRecord buildAccountRecord(@NonNull Context context, @NonNull Recipient self) {
|
||||||
RecipientSettings settings = DatabaseFactory.getRecipientDatabase(context).getRecipientSettingsForSync(self.getId());
|
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||||
|
RecipientSettings settings = recipientDatabase.getRecipientSettingsForSync(self.getId());
|
||||||
|
List<RecipientSettings> pinned = Stream.of(DatabaseFactory.getThreadDatabase(context).getPinnedRecipientIds())
|
||||||
|
.map(recipientDatabase::getRecipientSettingsForSync)
|
||||||
|
.toList();
|
||||||
|
|
||||||
SignalAccountRecord account = new SignalAccountRecord.Builder(self.getStorageServiceId())
|
SignalAccountRecord account = new SignalAccountRecord.Builder(self.getStorageServiceId())
|
||||||
.setUnknownFields(settings != null ? settings.getSyncExtras().getStorageProto() : null)
|
.setUnknownFields(settings != null ? settings.getSyncExtras().getStorageProto() : null)
|
||||||
|
@ -427,30 +431,13 @@ public final class StorageSyncHelper {
|
||||||
.setSealedSenderIndicatorsEnabled(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context))
|
.setSealedSenderIndicatorsEnabled(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context))
|
||||||
.setLinkPreviewsEnabled(SignalStore.settings().isLinkPreviewsEnabled())
|
.setLinkPreviewsEnabled(SignalStore.settings().isLinkPreviewsEnabled())
|
||||||
.setUnlistedPhoneNumber(SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode().isUnlisted())
|
.setUnlistedPhoneNumber(SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode().isUnlisted())
|
||||||
.setPhoneNumberSharingMode(localToRemotePhoneNumberSharingMode(SignalStore.phoneNumberPrivacy().getPhoneNumberSharingMode()))
|
.setPhoneNumberSharingMode(StorageSyncModels.localToRemotePhoneNumberSharingMode(SignalStore.phoneNumberPrivacy().getPhoneNumberSharingMode()))
|
||||||
|
.setPinnedConversations(StorageSyncModels.localToRemotePinnedConversations(pinned))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return SignalStorageRecord.forAccount(account);
|
return SignalStorageRecord.forAccount(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AccountRecord.PhoneNumberSharingMode localToRemotePhoneNumberSharingMode(PhoneNumberPrivacyValues.PhoneNumberSharingMode phoneNumberPhoneNumberSharingMode) {
|
|
||||||
switch (phoneNumberPhoneNumberSharingMode) {
|
|
||||||
case EVERYONE: return AccountRecord.PhoneNumberSharingMode.EVERYBODY;
|
|
||||||
case CONTACTS: return AccountRecord.PhoneNumberSharingMode.CONTACTS_ONLY;
|
|
||||||
case NOBODY : return AccountRecord.PhoneNumberSharingMode.NOBODY;
|
|
||||||
default : throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PhoneNumberPrivacyValues.PhoneNumberSharingMode remoteToLocalPhoneNumberSharingMode(AccountRecord.PhoneNumberSharingMode phoneNumberPhoneNumberSharingMode) {
|
|
||||||
switch (phoneNumberPhoneNumberSharingMode) {
|
|
||||||
case EVERYBODY : return PhoneNumberPrivacyValues.PhoneNumberSharingMode.EVERYONE;
|
|
||||||
case CONTACTS_ONLY: return PhoneNumberPrivacyValues.PhoneNumberSharingMode.CONTACTS;
|
|
||||||
case NOBODY : return PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY;
|
|
||||||
default : return PhoneNumberPrivacyValues.PhoneNumberSharingMode.CONTACTS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void applyAccountStorageSyncUpdates(@NonNull Context context, Optional<StorageSyncHelper.RecordUpdate<SignalAccountRecord>> update) {
|
public static void applyAccountStorageSyncUpdates(@NonNull Context context, Optional<StorageSyncHelper.RecordUpdate<SignalAccountRecord>> update) {
|
||||||
if (!update.isPresent()) {
|
if (!update.isPresent()) {
|
||||||
return;
|
return;
|
||||||
|
@ -466,7 +453,7 @@ public final class StorageSyncHelper {
|
||||||
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, update.isSealedSenderIndicatorsEnabled());
|
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, update.isSealedSenderIndicatorsEnabled());
|
||||||
SignalStore.settings().setLinkPreviewsEnabled(update.isLinkPreviewsEnabled());
|
SignalStore.settings().setLinkPreviewsEnabled(update.isLinkPreviewsEnabled());
|
||||||
SignalStore.phoneNumberPrivacy().setPhoneNumberListingMode(update.isPhoneNumberUnlisted() ? PhoneNumberPrivacyValues.PhoneNumberListingMode.UNLISTED : PhoneNumberPrivacyValues.PhoneNumberListingMode.LISTED);
|
SignalStore.phoneNumberPrivacy().setPhoneNumberListingMode(update.isPhoneNumberUnlisted() ? PhoneNumberPrivacyValues.PhoneNumberListingMode.UNLISTED : PhoneNumberPrivacyValues.PhoneNumberListingMode.LISTED);
|
||||||
SignalStore.phoneNumberPrivacy().setPhoneNumberSharingMode(remoteToLocalPhoneNumberSharingMode(update.getPhoneNumberSharingMode()));
|
SignalStore.phoneNumberPrivacy().setPhoneNumberSharingMode(StorageSyncModels.remoteToLocalPhoneNumberSharingMode(update.getPhoneNumberSharingMode()));
|
||||||
|
|
||||||
if (fetchProfile && update.getAvatarUrlPath().isPresent()) {
|
if (fetchProfile && update.getAvatarUrlPath().isPresent()) {
|
||||||
ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(Recipient.self(), update.getAvatarUrlPath().get()));
|
ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(Recipient.self(), update.getAvatarUrlPath().get()));
|
||||||
|
|
|
@ -2,18 +2,26 @@ package org.thoughtcrime.securesms.storage;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public final class StorageSyncModels {
|
public final class StorageSyncModels {
|
||||||
|
@ -37,6 +45,42 @@ public final class StorageSyncModels {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AccountRecord.PhoneNumberSharingMode localToRemotePhoneNumberSharingMode(PhoneNumberPrivacyValues.PhoneNumberSharingMode phoneNumberPhoneNumberSharingMode) {
|
||||||
|
switch (phoneNumberPhoneNumberSharingMode) {
|
||||||
|
case EVERYONE: return AccountRecord.PhoneNumberSharingMode.EVERYBODY;
|
||||||
|
case CONTACTS: return AccountRecord.PhoneNumberSharingMode.CONTACTS_ONLY;
|
||||||
|
case NOBODY : return AccountRecord.PhoneNumberSharingMode.NOBODY;
|
||||||
|
default : throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PhoneNumberPrivacyValues.PhoneNumberSharingMode remoteToLocalPhoneNumberSharingMode(AccountRecord.PhoneNumberSharingMode phoneNumberPhoneNumberSharingMode) {
|
||||||
|
switch (phoneNumberPhoneNumberSharingMode) {
|
||||||
|
case EVERYBODY : return PhoneNumberPrivacyValues.PhoneNumberSharingMode.EVERYONE;
|
||||||
|
case CONTACTS_ONLY: return PhoneNumberPrivacyValues.PhoneNumberSharingMode.CONTACTS;
|
||||||
|
case NOBODY : return PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY;
|
||||||
|
default : return PhoneNumberPrivacyValues.PhoneNumberSharingMode.CONTACTS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<SignalAccountRecord.PinnedConversation> localToRemotePinnedConversations(@NonNull List<RecipientSettings> settings) {
|
||||||
|
return Stream.of(settings)
|
||||||
|
.filter(s -> s.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V1 ||
|
||||||
|
s.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V2 ||
|
||||||
|
s.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED)
|
||||||
|
.map(StorageSyncModels::localToRemotePinnedConversation)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NonNull SignalAccountRecord.PinnedConversation localToRemotePinnedConversation(@NonNull RecipientSettings settings) {
|
||||||
|
switch (settings.getGroupType()) {
|
||||||
|
case NONE : return SignalAccountRecord.PinnedConversation.forContact(new SignalServiceAddress(settings.getUuid(), settings.getE164()));
|
||||||
|
case SIGNAL_V1: return SignalAccountRecord.PinnedConversation.forGroupV1(settings.getGroupId().requireV1().getDecodedId());
|
||||||
|
case SIGNAL_V2: return SignalAccountRecord.PinnedConversation.forGroupV2(settings.getSyncExtras().getGroupMasterKey().serialize());
|
||||||
|
default : throw new AssertionError("Unexpected group type!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static @NonNull SignalContactRecord localToRemoteContact(@NonNull RecipientSettings recipient, byte[] rawStorageId) {
|
private static @NonNull SignalContactRecord localToRemoteContact(@NonNull RecipientSettings recipient, byte[] rawStorageId) {
|
||||||
if (recipient.getUuid() == null && recipient.getE164() == null) {
|
if (recipient.getUuid() == null && recipient.getE164() == null) {
|
||||||
throw new AssertionError("Must have either a UUID or a phone number!");
|
throw new AssertionError("Must have either a UUID or a phone number!");
|
||||||
|
|
|
@ -3,10 +3,14 @@ package org.whispersystems.signalservice.api.storage;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||||
import org.whispersystems.signalservice.api.util.ProtoUtil;
|
import org.whispersystems.signalservice.api.util.ProtoUtil;
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public final class SignalAccountRecord implements SignalRecord {
|
public final class SignalAccountRecord implements SignalRecord {
|
||||||
|
@ -15,20 +19,26 @@ public final class SignalAccountRecord implements SignalRecord {
|
||||||
private final AccountRecord proto;
|
private final AccountRecord proto;
|
||||||
private final boolean hasUnknownFields;
|
private final boolean hasUnknownFields;
|
||||||
|
|
||||||
private final Optional<String> givenName;
|
private final Optional<String> givenName;
|
||||||
private final Optional<String> familyName;
|
private final Optional<String> familyName;
|
||||||
private final Optional<String> avatarUrlPath;
|
private final Optional<String> avatarUrlPath;
|
||||||
private final Optional<byte[]> profileKey;
|
private final Optional<byte[]> profileKey;
|
||||||
|
private final List<PinnedConversation> pinnedConversations;
|
||||||
|
|
||||||
public SignalAccountRecord(StorageId id, AccountRecord proto) {
|
public SignalAccountRecord(StorageId id, AccountRecord proto) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.proto = proto;
|
this.proto = proto;
|
||||||
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
|
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
|
||||||
|
|
||||||
this.givenName = OptionalUtil.absentIfEmpty(proto.getGivenName());
|
this.givenName = OptionalUtil.absentIfEmpty(proto.getGivenName());
|
||||||
this.familyName = OptionalUtil.absentIfEmpty(proto.getFamilyName());
|
this.familyName = OptionalUtil.absentIfEmpty(proto.getFamilyName());
|
||||||
this.profileKey = OptionalUtil.absentIfEmpty(proto.getProfileKey());
|
this.profileKey = OptionalUtil.absentIfEmpty(proto.getProfileKey());
|
||||||
this.avatarUrlPath = OptionalUtil.absentIfEmpty(proto.getAvatarUrlPath());
|
this.avatarUrlPath = OptionalUtil.absentIfEmpty(proto.getAvatarUrlPath());
|
||||||
|
this.pinnedConversations = new ArrayList<>(proto.getPinnedConversationsCount());
|
||||||
|
|
||||||
|
for (AccountRecord.PinnedConversation conversation : proto.getPinnedConversationsList()) {
|
||||||
|
pinnedConversations.add(PinnedConversation.fromRemote(conversation));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -92,6 +102,10 @@ public final class SignalAccountRecord implements SignalRecord {
|
||||||
return proto.getUnlistedPhoneNumber();
|
return proto.getUnlistedPhoneNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<PinnedConversation> getPinnedConversations() {
|
||||||
|
return pinnedConversations;
|
||||||
|
}
|
||||||
|
|
||||||
AccountRecord toProto() {
|
AccountRecord toProto() {
|
||||||
return proto;
|
return proto;
|
||||||
}
|
}
|
||||||
|
@ -110,6 +124,96 @@ public final class SignalAccountRecord implements SignalRecord {
|
||||||
return Objects.hash(id, proto);
|
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.parseOrNull(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();
|
||||||
|
if (contact.get().getUuid().isPresent()) {
|
||||||
|
contactBuilder.setUuid(contact.get().getUuid().get().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 final class Builder {
|
public static final class Builder {
|
||||||
private final StorageId id;
|
private final StorageId id;
|
||||||
private final AccountRecord.Builder builder;
|
private final AccountRecord.Builder builder;
|
||||||
|
@ -186,6 +290,16 @@ public final class SignalAccountRecord implements SignalRecord {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder setPinnedConversations(List<PinnedConversation> pinnedConversations) {
|
||||||
|
builder.clearPinnedConversations();
|
||||||
|
|
||||||
|
for (PinnedConversation pinned : pinnedConversations) {
|
||||||
|
builder.addPinnedConversations(pinned.toRemote());
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public SignalAccountRecord build() {
|
public SignalAccountRecord build() {
|
||||||
AccountRecord proto = builder.build();
|
AccountRecord proto = builder.build();
|
||||||
|
|
||||||
|
|
|
@ -107,17 +107,31 @@ message AccountRecord {
|
||||||
NOBODY = 2;
|
NOBODY = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes profileKey = 1;
|
message PinnedConversation {
|
||||||
string givenName = 2;
|
message Contact {
|
||||||
string familyName = 3;
|
string uuid = 1;
|
||||||
string avatarUrlPath = 4;
|
string e164 = 2;
|
||||||
bool noteToSelfArchived = 5;
|
}
|
||||||
bool readReceipts = 6;
|
|
||||||
bool sealedSenderIndicators = 7;
|
oneof identifier {
|
||||||
bool typingIndicators = 8;
|
Contact contact = 1;
|
||||||
bool proxiedLinkPreviews = 9;
|
bytes legacyGroupId = 3;
|
||||||
bool noteToSelfMarkedUnread = 10;
|
bytes groupMasterKey = 4;
|
||||||
bool linkPreviews = 11;
|
}
|
||||||
PhoneNumberSharingMode phoneNumberSharingMode = 12;
|
}
|
||||||
bool unlistedPhoneNumber = 13;
|
|
||||||
|
bytes profileKey = 1;
|
||||||
|
string givenName = 2;
|
||||||
|
string familyName = 3;
|
||||||
|
string avatarUrlPath = 4;
|
||||||
|
bool noteToSelfArchived = 5;
|
||||||
|
bool readReceipts = 6;
|
||||||
|
bool sealedSenderIndicators = 7;
|
||||||
|
bool typingIndicators = 8;
|
||||||
|
bool proxiedLinkPreviews = 9;
|
||||||
|
bool noteToSelfMarkedUnread = 10;
|
||||||
|
bool linkPreviews = 11;
|
||||||
|
PhoneNumberSharingMode phoneNumberSharingMode = 12;
|
||||||
|
bool unlistedPhoneNumber = 13;
|
||||||
|
repeated PinnedConversation pinnedConversations = 14;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue