Sync thread order and archive status with linked devices.
This commit is contained in:
parent
848101a783
commit
fe5fca8eaf
12 changed files with 217 additions and 74 deletions
|
@ -411,6 +411,8 @@ message ContactDetails {
|
|||
optional bytes profileKey = 6;
|
||||
optional bool blocked = 7;
|
||||
optional uint32 expireTimer = 8;
|
||||
optional uint32 inboxPosition = 10;
|
||||
optional bool archived = 11;
|
||||
}
|
||||
|
||||
message GroupDetails {
|
||||
|
@ -433,4 +435,6 @@ message GroupDetails {
|
|||
optional uint32 expireTimer = 6;
|
||||
optional string color = 7;
|
||||
optional bool blocked = 8;
|
||||
optional uint32 inboxPosition = 10;
|
||||
optional bool archived = 11;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,8 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
|
|||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
@ -176,29 +175,44 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
|
||||
try {
|
||||
DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactDataFile));
|
||||
Collection<ContactData> contacts = ContactAccessor.getInstance().getContactsWithPush(context);
|
||||
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);
|
||||
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(contactData.name);
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -93,12 +95,19 @@ public class MultiDeviceGroupUpdateJob extends BaseJob {
|
|||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue