Simplify storing storage-service-specific recipient values.

This gives us the ability to separate things we need for the Recipient
class from things we only need for storage syncing.

Not only does this simplify the storage service model building code
(i.e. we no longer need to pass around a set of archived recipients),
but it also eliminates a join on the Identity table for building regular
recipients, which should help perf.
This commit is contained in:
Greyson Parrelli
2020-10-06 10:24:14 -04:00
parent 38fa58c0a3
commit e00397620a
11 changed files with 181 additions and 143 deletions

View File

@@ -15,7 +15,6 @@ import net.sqlcipher.database.SQLiteConstraintException;
import net.sqlcipher.database.SQLiteDatabase;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
@@ -23,6 +22,7 @@ import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper.RecordUpdate;
import org.thoughtcrime.securesms.storage.StorageSyncModels;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.StringUtil;
@@ -61,6 +62,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -128,7 +130,7 @@ public class RecipientDatabase extends Database {
private static final String IDENTITY_KEY = "identity_key";
private static final String[] RECIPIENT_PROJECTION = new String[] {
UUID, USERNAME, PHONE, EMAIL, GROUP_ID, GROUP_TYPE,
ID, UUID, USERNAME, PHONE, EMAIL, GROUP_ID, GROUP_TYPE,
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
PROFILE_KEY, PROFILE_KEY_CREDENTIAL,
SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
@@ -144,20 +146,13 @@ public class RecipientDatabase extends Database {
private static final String[] ID_PROJECTION = new String[]{ID};
private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME};
public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, SEARCH_PROFILE_NAME, SORT_NAME};
static final String[] TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
private static final String[] TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
.map(columnName -> TABLE_NAME + "." + columnName)
.toList().toArray(new String[0]);
private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME};
static final String[] TYPED_RECIPIENT_PROJECTION_NO_ID = Arrays.copyOfRange(TYPED_RECIPIENT_PROJECTION, 1, TYPED_RECIPIENT_PROJECTION.length);
private static final String[] RECIPIENT_FULL_PROJECTION = Stream.of(
new String[] { TABLE_NAME + "." + ID,
TABLE_NAME + "." + STORAGE_PROTO },
TYPED_RECIPIENT_PROJECTION,
new String[] {
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS,
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY
}).flatMap(Stream::of).toArray(String[]::new);
private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME};
public static final String[] CREATE_INDEXS = new String[] {
"CREATE INDEX IF NOT EXISTS recipient_dirty_index ON " + TABLE_NAME + " (" + DIRTY + ");",
@@ -595,11 +590,10 @@ public class RecipientDatabase extends Database {
public @NonNull RecipientSettings getRecipientSettings(@NonNull RecipientId id) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID;
String query = TABLE_NAME + "." + ID + " = ?";
String query = ID + " = ?";
String[] args = new String[] { id.serialize() };
try (Cursor cursor = database.query(table, RECIPIENT_FULL_PROJECTION, query, args, null, null, null)) {
try (Cursor cursor = database.query(TABLE_NAME, RECIPIENT_PROJECTION, query, args, null, null, null)) {
if (cursor != null && cursor.moveToNext()) {
return getRecipientSettings(context, cursor);
} else {
@@ -814,12 +808,12 @@ public class RecipientDatabase extends Database {
Optional<IdentityRecord> newIdentityRecord = identityDatabase.getIdentity(recipientId);
if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED) &&
(!oldIdentityRecord.isPresent() || oldIdentityRecord.get().getVerifiedStatus() != IdentityDatabase.VerifiedStatus.VERIFIED))
if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED) &&
(!oldIdentityRecord.isPresent() || oldIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED))
{
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), true, true);
} else if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() != IdentityDatabase.VerifiedStatus.VERIFIED) &&
(oldIdentityRecord.isPresent() && oldIdentityRecord.get().getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED))
} else if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED) &&
(oldIdentityRecord.isPresent() && oldIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED))
{
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), false, true);
}
@@ -1054,12 +1048,20 @@ public class RecipientDatabase extends Database {
private List<RecipientSettings> getRecipientSettingsForSync(@Nullable String query, @Nullable String[] args) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID
+ " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + GROUP_ID + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID;
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID
+ " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + GROUP_ID + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID
+ " LEFT OUTER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID;
List<RecipientSettings> out = new ArrayList<>();
String[] columns = Stream.of(RECIPIENT_FULL_PROJECTION,
new String[]{GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY }).flatMap(Stream::of).toArray(String[]::new);
String[] columns = Stream.of(TYPED_RECIPIENT_PROJECTION,
new String[]{ RecipientDatabase.TABLE_NAME + "." + STORAGE_PROTO,
GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY,
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ARCHIVED,
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.READ,
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS,
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY })
.flatMap(Stream::of)
.toArray(String[]::new);
try (Cursor cursor = db.query(table, columns, query, args, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
@@ -1159,23 +1161,6 @@ public class RecipientDatabase extends Database {
int groupsV2CapabilityValue = CursorUtil.requireInt(cursor, GROUPS_V2_CAPABILITY);
String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID);
int mentionSettingId = CursorUtil.requireInt(cursor, MENTION_SETTING);
String storageProtoRaw = CursorUtil.getString(cursor, STORAGE_PROTO).orNull();
Optional<String> identityKeyRaw = CursorUtil.getString(cursor, IDENTITY_KEY);
Optional<Integer> identityStatusRaw = CursorUtil.getInt(cursor, IDENTITY_STATUS);
int masterKeyIndex = cursor.getColumnIndex(GroupDatabase.V2_MASTER_KEY);
GroupMasterKey groupMasterKey = null;
try {
if (masterKeyIndex != -1) {
byte[] blob = cursor.getBlob(masterKeyIndex);
if (blob != null) {
groupMasterKey = new GroupMasterKey(blob);
}
}
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
MaterialColor color;
byte[] profileKey = null;
@@ -1206,30 +1191,57 @@ public class RecipientDatabase extends Database {
}
}
byte[] storageKey = storageKeyRaw != null ? Base64.decodeOrThrow(storageKeyRaw) : null;
byte[] identityKey = identityKeyRaw.transform(Base64::decodeOrThrow).orNull();
byte[] storageProto = storageProtoRaw != null ? Base64.decodeOrThrow(storageProtoRaw) : null;
byte[] storageKey = storageKeyRaw != null ? Base64.decodeOrThrow(storageKeyRaw) : null;
IdentityDatabase.VerifiedStatus identityStatus = identityStatusRaw.transform(IdentityDatabase.VerifiedStatus::forState).or(IdentityDatabase.VerifiedStatus.DEFAULT);
return new RecipientSettings(RecipientId.from(id), uuid, username, e164, email, groupId, groupMasterKey, GroupType.fromId(groupType), blocked, muteUntil,
return new RecipientSettings(RecipientId.from(id),
uuid,
username,
e164,
email,
groupId,
GroupType.fromId(groupType),
blocked,
muteUntil,
VibrateState.fromId(messageVibrateState),
VibrateState.fromId(callVibrateState),
Util.uri(messageRingtone), Util.uri(callRingtone),
color, defaultSubscriptionId, expireMessages,
Util.uri(messageRingtone),
Util.uri(callRingtone),
color,
defaultSubscriptionId,
expireMessages,
RegisteredState.fromId(registeredState),
profileKey, profileKeyCredential,
systemDisplayName, systemContactPhoto,
systemPhoneLabel, systemContactUri,
ProfileName.fromParts(profileGivenName, profileFamilyName), signalProfileAvatar,
AvatarHelper.hasAvatar(context, RecipientId.from(id)), profileSharing, lastProfileFetch,
notificationChannel, UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
profileKey,
profileKeyCredential,
systemDisplayName,
systemContactPhoto,
systemPhoneLabel,
systemContactUri,
ProfileName.fromParts(profileGivenName, profileFamilyName),
signalProfileAvatar,
AvatarHelper.hasAvatar(context, RecipientId.from(id)),
profileSharing,
lastProfileFetch,
notificationChannel,
UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
forceSmsSelection,
Recipient.Capability.deserialize(uuidCapabilityValue),
Recipient.Capability.deserialize(groupsV2CapabilityValue),
InsightsBannerTier.fromId(insightsBannerTier),
storageKey, identityKey, identityStatus, MentionSetting.fromId(mentionSettingId),
storageProto);
storageKey,
MentionSetting.fromId(mentionSettingId),
getSyncExtras(cursor));
}
private static @NonNull RecipientSettings.SyncExtras getSyncExtras(@NonNull Cursor cursor) {
String storageProtoRaw = CursorUtil.getString(cursor, STORAGE_PROTO).orNull();
byte[] storageProto = storageProtoRaw != null ? Base64.decodeOrThrow(storageProtoRaw) : null;
boolean archived = CursorUtil.getBoolean(cursor, ThreadDatabase.ARCHIVED).or(false);
GroupMasterKey groupMasterKey = CursorUtil.getBlob(cursor, GroupDatabase.V2_MASTER_KEY).transform(GroupUtil::requireMasterKey).orNull();
byte[] identityKey = CursorUtil.getString(cursor, IDENTITY_KEY).transform(Base64::decodeOrThrow).orNull();
VerifiedStatus identityStatus = CursorUtil.getInt(cursor, IDENTITY_STATUS).transform(VerifiedStatus::forState).or(VerifiedStatus.DEFAULT);
return new RecipientSettings.SyncExtras(storageProto, groupMasterKey, identityKey, identityStatus, archived);
}
public BulkOperationsHandle beginBulkSystemContactUpdate() {
@@ -2537,7 +2549,6 @@ public class RecipientDatabase extends Database {
private final String e164;
private final String email;
private final GroupId groupId;
private final GroupMasterKey groupMasterKey;
private final GroupType groupType;
private final boolean blocked;
private final long muteUntil;
@@ -2567,10 +2578,8 @@ public class RecipientDatabase extends Database {
private final Recipient.Capability groupsV2Capability;
private final InsightsBannerTier insightsBannerTier;
private final byte[] storageId;
private final byte[] identityKey;
private final IdentityDatabase.VerifiedStatus identityStatus;
private final MentionSetting mentionSetting;
private final byte[] storageProto;
private final SyncExtras syncExtras;
RecipientSettings(@NonNull RecipientId id,
@Nullable UUID uuid,
@@ -2578,7 +2587,6 @@ public class RecipientDatabase extends Database {
@Nullable String e164,
@Nullable String email,
@Nullable GroupId groupId,
@Nullable GroupMasterKey groupMasterKey,
@NonNull GroupType groupType,
boolean blocked,
long muteUntil,
@@ -2608,10 +2616,8 @@ public class RecipientDatabase extends Database {
Recipient.Capability groupsV2Capability,
@NonNull InsightsBannerTier insightsBannerTier,
@Nullable byte[] storageId,
@Nullable byte[] identityKey,
@NonNull IdentityDatabase.VerifiedStatus identityStatus,
@NonNull MentionSetting mentionSetting,
@Nullable byte[] storageProto)
@NonNull SyncExtras syncExtras)
{
this.id = id;
this.uuid = uuid;
@@ -2619,7 +2625,6 @@ public class RecipientDatabase extends Database {
this.e164 = e164;
this.email = email;
this.groupId = groupId;
this.groupMasterKey = groupMasterKey;
this.groupType = groupType;
this.blocked = blocked;
this.muteUntil = muteUntil;
@@ -2649,10 +2654,8 @@ public class RecipientDatabase extends Database {
this.groupsV2Capability = groupsV2Capability;
this.insightsBannerTier = insightsBannerTier;
this.storageId = storageId;
this.identityKey = identityKey;
this.identityStatus = identityStatus;
this.mentionSetting = mentionSetting;
this.storageProto = storageProto;
this.syncExtras = syncExtras;
}
public RecipientId getId() {
@@ -2679,13 +2682,6 @@ public class RecipientDatabase extends Database {
return groupId;
}
/**
* Only read populated for sync.
*/
public @Nullable GroupMasterKey getGroupMasterKey() {
return groupMasterKey;
}
public @NonNull GroupType getGroupType() {
return groupType;
}
@@ -2802,20 +2798,57 @@ public class RecipientDatabase extends Database {
return storageId;
}
public @Nullable byte[] getIdentityKey() {
return identityKey;
}
public @NonNull IdentityDatabase.VerifiedStatus getIdentityStatus() {
return identityStatus;
}
public @NonNull MentionSetting getMentionSetting() {
return mentionSetting;
}
public @Nullable byte[] getStorageProto() {
return storageProto;
public @NonNull SyncExtras getSyncExtras() {
return syncExtras;
}
/**
* A bundle of data that's only necessary when syncing to storage service, not for a
* {@link Recipient}.
*/
public static class SyncExtras {
private final byte[] storageProto;
private final GroupMasterKey groupMasterKey;
private final byte[] identityKey;
private final VerifiedStatus identityStatus;
private final boolean archived;
public SyncExtras(@Nullable byte[] storageProto,
@Nullable GroupMasterKey groupMasterKey,
@Nullable byte[] identityKey,
@NonNull VerifiedStatus identityStatus,
boolean archived)
{
this.storageProto = storageProto;
this.groupMasterKey = groupMasterKey;
this.identityKey = identityKey;
this.identityStatus = identityStatus;
this.archived = archived;
}
public @Nullable byte[] getStorageProto() {
return storageProto;
}
public @Nullable GroupMasterKey getGroupMasterKey() {
return groupMasterKey;
}
public boolean isArchived() {
return archived;
}
public @Nullable byte[] getIdentityKey() {
return identityKey;
}
public @NonNull VerifiedStatus getIdentityStatus() {
return identityStatus;
}
}
}

View File

@@ -144,7 +144,7 @@ public class ThreadDatabase extends Database {
.toList();
private static final List<String> COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION = Stream.concat(Stream.concat(Stream.of(TYPED_THREAD_PROJECTION),
Stream.of(RecipientDatabase.TYPED_RECIPIENT_PROJECTION)),
Stream.of(RecipientDatabase.TYPED_RECIPIENT_PROJECTION_NO_ID)),
Stream.of(GroupDatabase.TYPED_GROUP_PROJECTION))
.toList();