Sync thread order and archive status with linked devices.

This commit is contained in:
Greyson Parrelli 2019-12-18 00:44:21 -05:00 committed by Alan Evans
parent 848101a783
commit fe5fca8eaf
12 changed files with 217 additions and 74 deletions

View file

@ -402,15 +402,17 @@ message ContactDetails {
optional uint32 length = 2;
}
optional string number = 1;
optional string uuid = 9;
optional string name = 2;
optional Avatar avatar = 3;
optional string color = 4;
optional Verified verified = 5;
optional bytes profileKey = 6;
optional bool blocked = 7;
optional uint32 expireTimer = 8;
optional string number = 1;
optional string uuid = 9;
optional string name = 2;
optional Avatar avatar = 3;
optional string color = 4;
optional Verified verified = 5;
optional bytes profileKey = 6;
optional bool blocked = 7;
optional uint32 expireTimer = 8;
optional uint32 inboxPosition = 10;
optional bool archived = 11;
}
message GroupDetails {
@ -424,13 +426,15 @@ message GroupDetails {
optional string e164 = 2;
}
optional bytes id = 1;
optional string name = 2;
repeated string membersE164 = 3;
repeated Member members = 9;
optional Avatar avatar = 4;
optional bool active = 5 [default = true];
optional uint32 expireTimer = 6;
optional string color = 7;
optional bool blocked = 8;
optional bytes id = 1;
optional string name = 2;
repeated string membersE164 = 3;
repeated Member members = 9;
optional Avatar avatar = 4;
optional bool active = 5 [default = true];
optional uint32 expireTimer = 6;
optional string color = 7;
optional bool blocked = 8;
optional uint32 inboxPosition = 10;
optional bool archived = 11;
}

View file

@ -20,14 +20,19 @@ public class DeviceContact {
private final Optional<byte[]> profileKey;
private final boolean blocked;
private final Optional<Integer> expirationTimer;
private final Optional<Integer> inboxPosition;
private final boolean archived;
public DeviceContact(SignalServiceAddress address, Optional<String> name,
public DeviceContact(SignalServiceAddress address,
Optional<String> name,
Optional<SignalServiceAttachmentStream> avatar,
Optional<String> color,
Optional<VerifiedMessage> verified,
Optional<byte[]> profileKey,
boolean blocked,
Optional<Integer> expirationTimer)
Optional<Integer> expirationTimer,
Optional<Integer> inboxPosition,
boolean archived)
{
this.address = address;
this.name = name;
@ -37,6 +42,8 @@ public class DeviceContact {
this.profileKey = profileKey;
this.blocked = blocked;
this.expirationTimer = expirationTimer;
this.inboxPosition = inboxPosition;
this.archived = archived;
}
public Optional<SignalServiceAttachmentStream> getAvatar() {
@ -70,4 +77,12 @@ public class DeviceContact {
public Optional<Integer> getExpirationTimer() {
return expirationTimer;
}
public Optional<Integer> getInboxPosition() {
return inboxPosition;
}
public boolean isArchived() {
return archived;
}
}

View file

@ -39,14 +39,16 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
throw new IOException("Missing contact address!");
}
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(details.getUuid()), details.getNumber());
Optional<String> name = Optional.fromNullable(details.getName());
Optional<SignalServiceAttachmentStream> avatar = Optional.absent();
Optional<String> color = details.hasColor() ? Optional.of(details.getColor()) : Optional.<String>absent();
Optional<VerifiedMessage> verified = Optional.absent();
Optional<byte[]> profileKey = Optional.absent();
boolean blocked = false;
Optional<Integer> expireTimer = Optional.absent();
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(details.getUuid()), details.getNumber());
Optional<String> name = Optional.fromNullable(details.getName());
Optional<SignalServiceAttachmentStream> avatar = Optional.absent();
Optional<String> color = details.hasColor() ? Optional.of(details.getColor()) : Optional.<String>absent();
Optional<VerifiedMessage> verified = Optional.absent();
Optional<byte[]> profileKey = Optional.absent();
boolean blocked = false;
Optional<Integer> expireTimer = Optional.absent();
Optional<Integer> inboxPosition = Optional.absent();
boolean archived = false;
if (details.hasAvatar()) {
long avatarLength = details.getAvatar().getLength();
@ -89,9 +91,14 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
expireTimer = Optional.of(details.getExpireTimer());
}
blocked = details.getBlocked();
if (details.hasInboxPosition()) {
inboxPosition = Optional.of(details.getInboxPosition());
}
return new DeviceContact(address, name, avatar, color, verified, profileKey, blocked, expireTimer);
blocked = details.getBlocked();
archived = details.getArchived();
return new DeviceContact(address, name, avatar, color, verified, profileKey, blocked, expireTimer, inboxPosition, archived);
}
}

View file

@ -92,7 +92,12 @@ public class DeviceContactsOutputStream extends ChunkedOutputStream {
contactDetails.setExpireTimer(contact.getExpirationTimer().get());
}
if (contact.getInboxPosition().isPresent()) {
contactDetails.setInboxPosition(contact.getInboxPosition().get());
}
contactDetails.setBlocked(contact.isBlocked());
contactDetails.setArchived(contact.isArchived());
byte[] serializedContactDetails = contactDetails.build().toByteArray();

View file

@ -22,11 +22,19 @@ public class DeviceGroup {
private final Optional<Integer> expirationTimer;
private final Optional<String> color;
private final boolean blocked;
private final Optional<Integer> inboxPosition;
private final boolean archived;
public DeviceGroup(byte[] id, Optional<String> name, List<SignalServiceAddress> members,
public DeviceGroup(byte[] id,
Optional<String> name,
List<SignalServiceAddress> members,
Optional<SignalServiceAttachmentStream> avatar,
boolean active, Optional<Integer> expirationTimer,
Optional<String> color, boolean blocked)
boolean active,
Optional<Integer> expirationTimer,
Optional<String> color,
boolean blocked,
Optional<Integer> inboxPosition,
boolean archived)
{
this.id = id;
this.name = name;
@ -36,6 +44,8 @@ public class DeviceGroup {
this.expirationTimer = expirationTimer;
this.color = color;
this.blocked = blocked;
this.inboxPosition = inboxPosition;
this.archived = archived;
}
public Optional<SignalServiceAttachmentStream> getAvatar() {
@ -69,4 +79,12 @@ public class DeviceGroup {
public boolean isBlocked() {
return blocked;
}
public Optional<Integer> getInboxPosition() {
return inboxPosition;
}
public boolean isArchived() {
return archived;
}
}

View file

@ -44,6 +44,8 @@ public class DeviceGroupsInputStream extends ChunkedInputStream{
Optional<Integer> expirationTimer = Optional.absent();
Optional<String> color = Optional.fromNullable(details.getColor());
boolean blocked = details.getBlocked();
Optional<Integer> inboxPosition = Optional.absent();
boolean archived = false;
if (details.hasAvatar()) {
long avatarLength = details.getAvatar().getLength();
@ -66,7 +68,15 @@ public class DeviceGroupsInputStream extends ChunkedInputStream{
}
}
return new DeviceGroup(id, name, addressMembers, avatar, active, expirationTimer, color, blocked);
if (details.hasInboxPosition()) {
inboxPosition = Optional.of(details.getInboxPosition());
}
if (details.hasArchived()) {
archived = details.getArchived();
}
return new DeviceGroup(id, name, addressMembers, avatar, active, expirationTimer, color, blocked, inboxPosition, archived);
}
}

View file

@ -83,6 +83,11 @@ public class DeviceGroupsOutputStream extends ChunkedOutputStream {
groupDetails.addAllMembersE164(membersE164);
groupDetails.setActive(group.isActive());
groupDetails.setBlocked(group.isBlocked());
groupDetails.setArchived(group.isArchived());
if (group.getInboxPosition().isPresent()) {
groupDetails.setInboxPosition(group.getInboxPosition().get());
}
byte[] serializedContactDetails = groupDetails.build().toByteArray();

View file

@ -1075,6 +1075,25 @@ public class RecipientDatabase extends Database {
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null);
}
public @NonNull List<Recipient> getRecipientsForMultiDeviceSync() {
String subquery = "SELECT " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " FROM " + ThreadDatabase.TABLE_NAME;
String selection = REGISTERED + " = ? AND " +
GROUP_ID + " IS NULL AND " +
ID + " != ? AND " +
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + ID + " IN (" + subquery + "))";
String[] args = new String[] { String.valueOf(RegisteredState.REGISTERED.getId()), Recipient.self().getId().serialize() };
List<Recipient> recipients = new ArrayList<>();
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, ID_PROJECTION, selection, args, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
recipients.add(Recipient.resolved(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))));
}
}
return recipients;
}
public void applyBlockedUpdate(@NonNull List<SignalServiceAddress> blocked, List<byte[]> groupIds) {
List<String> blockedE164 = Stream.of(blocked)
.filter(b -> b.getNumber().isPresent())

View file

@ -52,8 +52,11 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.Closeable;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ThreadDatabase extends Database {
@ -418,6 +421,36 @@ public class ThreadDatabase extends Database {
return getConversationList("1");
}
public @NonNull Set<RecipientId> getArchivedRecipients() {
Set<RecipientId> archived = new HashSet<>();
try (Cursor cursor = DatabaseFactory.getThreadDatabase(context).getArchivedConversationList()) {
while (cursor != null && cursor.moveToNext()) {
archived.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.RECIPIENT_ID))));
}
}
return archived;
}
public @NonNull Map<RecipientId, Integer> getInboxPositions() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = createQuery(MESSAGE_COUNT + " != ?", 0);
Map<RecipientId, Integer> positions = new HashMap<>();
try (Cursor cursor = db.rawQuery(query, new String[] { "0" })) {
int i = 0;
while (cursor != null && cursor.moveToNext()) {
RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.RECIPIENT_ID)));
positions.put(recipientId, i);
i++;
}
}
return positions;
}
private Cursor getConversationList(String archived) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = createQuery(ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0", 0);

View file

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.jobs;
import android.Manifest;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.net.Uri;
@ -9,18 +8,16 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
@ -45,7 +42,11 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class MultiDeviceContactUpdateJob extends BaseJob {
@ -128,17 +129,20 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
Recipient recipient = Recipient.resolved(recipientId);
Optional<IdentityDatabase.IdentityRecord> identityRecord = DatabaseFactory.getIdentityDatabase(context).getIdentity(recipient.getId());
Optional<VerifiedMessage> verifiedMessage = getVerifiedMessage(recipient, identityRecord);
Map<RecipientId, Integer> inboxPositions = DatabaseFactory.getThreadDatabase(context).getInboxPositions();
Set<RecipientId> archived = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
out.write(new DeviceContact(RecipientUtil.toSignalServiceAddress(context, recipient),
Optional.of(recipient.getDisplayName(context)),
getAvatar(recipient.getContactUri()),
getSystemAvatar(recipient.getContactUri()),
Optional.fromNullable(recipient.getColor().serialize()),
verifiedMessage,
Optional.fromNullable(recipient.getProfileKey()),
recipient.isBlocked(),
recipient.getExpireMessages() > 0 ?
Optional.of(recipient.getExpireMessages()) :
Optional.absent()));
recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages())
: Optional.absent(),
Optional.fromNullable(inboxPositions.get(recipientId)),
archived.contains(recipientId)));
out.close();
sendUpdate(ApplicationDependencies.getSignalServiceMessageSender(), contactDataFile, false);
@ -153,11 +157,6 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
private void generateFullContactUpdate()
throws IOException, UntrustedIdentityException, NetworkException
{
if (!Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
Log.w(TAG, "No contact permissions, skipping multi-device contact update...");
return;
}
boolean isAppVisible = ApplicationContext.getInstance(context).isAppVisible();
long timeSinceLastSync = System.currentTimeMillis() - TextSecurePreferences.getLastFullContactSyncTime(context);
@ -175,30 +174,45 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
File contactDataFile = createTempFile("multidevice-contact-update");
try {
DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactDataFile));
Collection<ContactData> contacts = ContactAccessor.getInstance().getContactsWithPush(context);
DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactDataFile));
List<Recipient> recipients = DatabaseFactory.getRecipientDatabase(context).getRecipientsForMultiDeviceSync();
Map<RecipientId, Integer> inboxPositions = DatabaseFactory.getThreadDatabase(context).getInboxPositions();
Set<RecipientId> archived = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
for (ContactData contactData : contacts) {
Uri contactUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, String.valueOf(contactData.id));
Recipient recipient = Recipient.external(context, contactData.numbers.get(0).number);
Optional<IdentityDatabase.IdentityRecord> identity = DatabaseFactory.getIdentityDatabase(context).getIdentity(recipient.getId());
Optional<VerifiedMessage> verified = getVerifiedMessage(recipient, identity);
Optional<String> name = Optional.fromNullable(contactData.name);
Optional<String> color = Optional.of(recipient.getColor().serialize());
Optional<byte[]> profileKey = Optional.fromNullable(recipient.getProfileKey());
boolean blocked = recipient.isBlocked();
Optional<Integer> expireTimer = recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages()) : Optional.absent();
for (Recipient recipient : recipients) {
Optional<IdentityDatabase.IdentityRecord> identity = DatabaseFactory.getIdentityDatabase(context).getIdentity(recipient.getId());
Optional<VerifiedMessage> verified = getVerifiedMessage(recipient, identity);
Optional<String> name = Optional.fromNullable(recipient.getName(context));
Optional<String> color = Optional.of(recipient.getColor().serialize());
Optional<byte[]> profileKey = Optional.fromNullable(recipient.getProfileKey());
boolean blocked = recipient.isBlocked();
Optional<Integer> expireTimer = recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages()) : Optional.absent();
Optional<Integer> inboxPosition = Optional.fromNullable(inboxPositions.get(recipient.getId()));
out.write(new DeviceContact(RecipientUtil.toSignalServiceAddress(context, recipient), name, getAvatar(contactUri), color, verified, profileKey, blocked, expireTimer));
out.write(new DeviceContact(RecipientUtil.toSignalServiceAddress(context, recipient),
name,
getSystemAvatar(recipient.getContactUri()),
color,
verified,
profileKey,
blocked,
expireTimer,
inboxPosition,
archived.contains(recipient.getId())));
}
if (ProfileKeyUtil.hasProfileKey(context)) {
Recipient self = Recipient.self();
out.write(new DeviceContact(RecipientUtil.toSignalServiceAddress(context, Recipient.self()),
Optional.absent(), Optional.absent(),
Optional.of(self.getColor().serialize()), Optional.absent(),
out.write(new DeviceContact(RecipientUtil.toSignalServiceAddress(context, self),
Optional.absent(),
Optional.absent(),
Optional.of(self.getColor().serialize()),
Optional.absent(),
Optional.of(ProfileKeyUtil.getProfileKey(context)),
false, self.getExpireMessages() > 0 ? Optional.of(self.getExpireMessages()) : Optional.absent()));
false,
self.getExpireMessages() > 0 ? Optional.of(self.getExpireMessages()) : Optional.absent(),
Optional.fromNullable(inboxPositions.get(self.getId())),
archived.contains(self.getId())));
}
out.close();
@ -241,7 +255,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
}
}
private Optional<SignalServiceAttachmentStream> getAvatar(@Nullable Uri uri) throws IOException {
private Optional<SignalServiceAttachmentStream> getSystemAvatar(@Nullable Uri uri) {
if (uri == null) {
return Optional.absent();
}

View file

@ -34,6 +34,8 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class MultiDeviceGroupUpdateJob extends BaseJob {
@ -90,15 +92,22 @@ public class MultiDeviceGroupUpdateJob extends BaseJob {
members.add(RecipientUtil.toSignalServiceAddress(context, Recipient.resolved(member)));
}
RecipientId recipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(GroupUtil.getEncodedId(record.getId(), record.isMms()));
Recipient recipient = Recipient.resolved(recipientId);
Optional<Integer> expirationTimer = recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages()) : Optional.absent();
RecipientId recipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(GroupUtil.getEncodedId(record.getId(), record.isMms()));
Recipient recipient = Recipient.resolved(recipientId);
Optional<Integer> expirationTimer = recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages()) : Optional.absent();
Map<RecipientId, Integer> inboxPositions = DatabaseFactory.getThreadDatabase(context).getInboxPositions();
Set<RecipientId> archived = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
out.write(new DeviceGroup(record.getId(), Optional.fromNullable(record.getTitle()),
members, getAvatar(record.getAvatar()),
record.isActive(), expirationTimer,
out.write(new DeviceGroup(record.getId(),
Optional.fromNullable(record.getTitle()),
members,
getAvatar(record.getAvatar()),
record.isActive(),
expirationTimer,
Optional.of(recipient.getColor().serialize()),
recipient.isBlocked()));
recipient.isBlocked(),
Optional.fromNullable(inboxPositions.get(recipientId)),
archived.contains(recipientId)));
}
}

View file

@ -75,7 +75,11 @@ public class MultiDeviceProfileKeyUpdateJob extends BaseJob {
Optional.absent(),
Optional.absent(),
Optional.absent(),
profileKey, false, Optional.absent()));
profileKey,
false,
Optional.absent(),
Optional.absent(),
false));
out.close();