Handle UUID-only recipients and merging.

This commit is contained in:
Greyson Parrelli 2020-07-15 18:03:18 -04:00
parent 644af87782
commit bd078fc883
23 changed files with 1026 additions and 237 deletions

View File

@ -59,6 +59,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
@ -102,32 +103,14 @@ public class DirectoryHelper {
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
}
HashMap<RecipientId, String> uuidMap = new HashMap<>();
// TODO [greyson] [cds] Probably want to do this in a DB transaction to prevent concurrent operations
for (Map.Entry<String, UUID> entry : result.getRegisteredNumbers().entrySet()) {
// TODO [greyson] [cds] This is where we'll have to do record merging
String e164 = entry.getKey();
UUID uuid = entry.getValue();
Optional<RecipientId> uuidEntry = uuid != null ? recipientDatabase.getByUuid(uuid) : Optional.absent();
// TODO [greyson] [cds] Handle phone numbers changing, possibly conflicting
if (uuidEntry.isPresent()) {
recipientDatabase.setPhoneNumber(uuidEntry.get(), e164);
}
RecipientId id = uuidEntry.isPresent() ? uuidEntry.get() : recipientDatabase.getOrInsertFromE164(e164);
uuidMap.put(id, uuid != null ? uuid.toString() : null);
}
Set<String> activeNumbers = result.getRegisteredNumbers().keySet();
Set<RecipientId> activeIds = uuidMap.keySet();
Set<RecipientId> inactiveIds = Stream.of(allNumbers)
.filterNot(activeNumbers::contains)
.filterNot(n -> result.getNumberRewrites().containsKey(n))
.map(recipientDatabase::getOrInsertFromE164)
.collect(Collectors.toSet());
Map<RecipientId, String> uuidMap = recipientDatabase.bulkProcessCdsResult(result.getRegisteredNumbers());
Set<String> activeNumbers = result.getRegisteredNumbers().keySet();
Set<RecipientId> activeIds = uuidMap.keySet();
Set<RecipientId> inactiveIds = Stream.of(allNumbers)
.filterNot(activeNumbers::contains)
.filterNot(n -> result.getNumberRewrites().containsKey(n))
.map(recipientDatabase::getOrInsertFromE164)
.collect(Collectors.toSet());
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
@ -162,7 +145,10 @@ public class DirectoryHelper {
if (recipient.hasUuid() && !recipient.hasE164()) {
boolean isRegistered = isUuidRegistered(context, recipient);
if (isRegistered) {
recipientDatabase.markRegistered(recipient.getId(), recipient.getUuid().get());
boolean idChanged = recipientDatabase.markRegistered(recipient.getId(), recipient.getUuid().get());
if (idChanged) {
Log.w(TAG, "ID changed during refresh by UUID.");
}
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
@ -189,7 +175,15 @@ public class DirectoryHelper {
}
if (result.getRegisteredNumbers().size() > 0) {
recipientDatabase.markRegistered(recipient.getId(), result.getRegisteredNumbers().values().iterator().next());
UUID uuid = result.getRegisteredNumbers().values().iterator().next();
if (uuid != null) {
boolean idChanged = recipientDatabase.markRegistered(recipient.getId(), uuid);
if (idChanged) {
recipient = Recipient.resolved(recipientDatabase.getByUuid(uuid).get());
}
} else {
recipientDatabase.markRegistered(recipient.getId());
}
} else {
recipientDatabase.markUnregistered(recipient.getId());
}

View File

@ -39,29 +39,30 @@ public class DatabaseFactory {
private static DatabaseFactory instance;
private final SQLCipherOpenHelper databaseHelper;
private final SmsDatabase sms;
private final MmsDatabase mms;
private final AttachmentDatabase attachments;
private final MediaDatabase media;
private final ThreadDatabase thread;
private final MmsSmsDatabase mmsSmsDatabase;
private final IdentityDatabase identityDatabase;
private final DraftDatabase draftDatabase;
private final PushDatabase pushDatabase;
private final GroupDatabase groupDatabase;
private final RecipientDatabase recipientDatabase;
private final ContactsDatabase contactsDatabase;
private final GroupReceiptDatabase groupReceiptDatabase;
private final OneTimePreKeyDatabase preKeyDatabase;
private final SignedPreKeyDatabase signedPreKeyDatabase;
private final SessionDatabase sessionDatabase;
private final SearchDatabase searchDatabase;
private final JobDatabase jobDatabase;
private final StickerDatabase stickerDatabase;
private final StorageKeyDatabase storageKeyDatabase;
private final KeyValueDatabase keyValueDatabase;
private final MegaphoneDatabase megaphoneDatabase;
private final SQLCipherOpenHelper databaseHelper;
private final SmsDatabase sms;
private final MmsDatabase mms;
private final AttachmentDatabase attachments;
private final MediaDatabase media;
private final ThreadDatabase thread;
private final MmsSmsDatabase mmsSmsDatabase;
private final IdentityDatabase identityDatabase;
private final DraftDatabase draftDatabase;
private final PushDatabase pushDatabase;
private final GroupDatabase groupDatabase;
private final RecipientDatabase recipientDatabase;
private final ContactsDatabase contactsDatabase;
private final GroupReceiptDatabase groupReceiptDatabase;
private final OneTimePreKeyDatabase preKeyDatabase;
private final SignedPreKeyDatabase signedPreKeyDatabase;
private final SessionDatabase sessionDatabase;
private final SearchDatabase searchDatabase;
private final JobDatabase jobDatabase;
private final StickerDatabase stickerDatabase;
private final StorageKeyDatabase storageKeyDatabase;
private final KeyValueDatabase keyValueDatabase;
private final MegaphoneDatabase megaphoneDatabase;
private final RemappedRecordsDatabase remappedRecordsDatabase;
public static DatabaseFactory getInstance(Context context) {
synchronized (lock) {
@ -160,6 +161,10 @@ public class DatabaseFactory {
return getInstance(context).megaphoneDatabase;
}
static RemappedRecordsDatabase getRemappedRecordsDatabase(Context context) {
return getInstance(context).remappedRecordsDatabase;
}
public static SQLiteDatabase getBackupDatabase(Context context) {
return getInstance(context).databaseHelper.getReadableDatabase();
}
@ -175,8 +180,8 @@ public class DatabaseFactory {
}
}
static SQLCipherOpenHelper getRawDatabase(Context context) {
return getInstance(context).databaseHelper;
public static boolean inTransaction(Context context) {
return getInstance(context).databaseHelper.getWritableDatabase().inTransaction();
}
private DatabaseFactory(@NonNull Context context) {
@ -185,29 +190,30 @@ public class DatabaseFactory {
DatabaseSecret databaseSecret = new DatabaseSecretProvider(context).getOrCreateDatabaseSecret();
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret);
this.sms = new SmsDatabase(context, databaseHelper);
this.mms = new MmsDatabase(context, databaseHelper);
this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret);
this.media = new MediaDatabase(context, databaseHelper);
this.thread = new ThreadDatabase(context, databaseHelper);
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
this.draftDatabase = new DraftDatabase(context, databaseHelper);
this.pushDatabase = new PushDatabase(context, databaseHelper);
this.groupDatabase = new GroupDatabase(context, databaseHelper);
this.recipientDatabase = new RecipientDatabase(context, databaseHelper);
this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper);
this.contactsDatabase = new ContactsDatabase(context);
this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper);
this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper);
this.sessionDatabase = new SessionDatabase(context, databaseHelper);
this.searchDatabase = new SearchDatabase(context, databaseHelper);
this.jobDatabase = new JobDatabase(context, databaseHelper);
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
this.storageKeyDatabase = new StorageKeyDatabase(context, databaseHelper);
this.keyValueDatabase = new KeyValueDatabase(context, databaseHelper);
this.megaphoneDatabase = new MegaphoneDatabase(context, databaseHelper);
this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret);
this.sms = new SmsDatabase(context, databaseHelper);
this.mms = new MmsDatabase(context, databaseHelper);
this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret);
this.media = new MediaDatabase(context, databaseHelper);
this.thread = new ThreadDatabase(context, databaseHelper);
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
this.draftDatabase = new DraftDatabase(context, databaseHelper);
this.pushDatabase = new PushDatabase(context, databaseHelper);
this.groupDatabase = new GroupDatabase(context, databaseHelper);
this.recipientDatabase = new RecipientDatabase(context, databaseHelper);
this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper);
this.contactsDatabase = new ContactsDatabase(context);
this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper);
this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper);
this.sessionDatabase = new SessionDatabase(context, databaseHelper);
this.searchDatabase = new SearchDatabase(context, databaseHelper);
this.jobDatabase = new JobDatabase(context, databaseHelper);
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
this.storageKeyDatabase = new StorageKeyDatabase(context, databaseHelper);
this.keyValueDatabase = new KeyValueDatabase(context, databaseHelper);
this.megaphoneDatabase = new MegaphoneDatabase(context, databaseHelper);
this.remappedRecordsDatabase = new RemappedRecordsDatabase(context, databaseHelper);
}
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,

View File

@ -17,7 +17,7 @@ import java.util.Set;
public class DraftDatabase extends Database {
private static final String TABLE_NAME = "drafts";
static final String TABLE_NAME = "drafts";
public static final String ID = "_id";
public static final String THREAD_ID = "thread_id";
public static final String DRAFT_TYPE = "type";

View File

@ -53,7 +53,7 @@ public final class GroupDatabase extends Database {
static final String GROUP_ID = "group_id";
static final String RECIPIENT_ID = "recipient_id";
private static final String TITLE = "title";
private static final String MEMBERS = "members";
static final String MEMBERS = "members";
private static final String AVATAR_ID = "avatar_id";
private static final String AVATAR_KEY = "avatar_key";
private static final String AVATAR_CONTENT_TYPE = "avatar_content_type";

View File

@ -23,7 +23,7 @@ public class GroupReceiptDatabase extends Database {
private static final String ID = "_id";
public static final String MMS_ID = "mms_id";
private static final String RECIPIENT_ID = "address";
static final String RECIPIENT_ID = "address";
private static final String STATUS = "status";
private static final String TIMESTAMP = "timestamp";
private static final String UNIDENTIFIED = "unidentified";

View File

@ -12,6 +12,7 @@ import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import com.google.android.gms.common.util.ArrayUtils;
import net.sqlcipher.database.SQLiteConstraintException;
import net.sqlcipher.database.SQLiteDatabase;
import org.signal.zkgroup.InvalidInputException;
@ -44,6 +45,7 @@ import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -66,6 +68,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -358,6 +361,156 @@ public class RecipientDatabase extends Database {
return getByColumn(USERNAME, username);
}
public @NonNull RecipientId getAndPossiblyMerge(@Nullable UUID uuid, @Nullable String e164, boolean highTrust) {
if (uuid == null && e164 == null) {
throw new IllegalArgumentException("Must provide a UUID or E164!");
}
RecipientId recipientNeedingRefresh = null;
Pair<RecipientId, RecipientId> remapped = null;
boolean transactionSuccessful = false;
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
Optional<RecipientId> byE164 = e164 != null ? getByE164(e164) : Optional.absent();
Optional<RecipientId> byUuid = uuid != null ? getByUuid(uuid) : Optional.absent();
RecipientId finalId;
if (!byE164.isPresent() && !byUuid.isPresent()) {
Log.i(TAG, "Discovered a completely new user. Inserting.");
if (highTrust) {
long id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(e164, uuid));
finalId = RecipientId.from(id);
} else {
long id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(uuid == null ? e164 : null, uuid));
finalId = RecipientId.from(id);
}
} else if (byE164.isPresent() && !byUuid.isPresent()) {
if (uuid != null) {
RecipientSettings e164Settings = getRecipientSettings(byE164.get());
if (e164Settings.uuid != null) {
if (highTrust) {
Log.w(TAG, "Found out about a UUID for a known E164 user, but that user already has a UUID. Likely a case of re-registration. High-trust, so stripping the E164 from the existing account and assigning it to a new entry.");
removePhoneNumber(byE164.get(), db);
recipientNeedingRefresh = byE164.get();
ContentValues insertValues = buildContentValuesForNewUser(e164, uuid);
insertValues.put(BLOCKED, e164Settings.blocked ? 1 : 0);
long id = db.insert(TABLE_NAME, null, insertValues);
finalId = RecipientId.from(id);
} else {
Log.w(TAG, "Found out about a UUID for a known E164 user, but that user already has a UUID. Likely a case of re-registration. Low-trust, so making a new user for the UUID.");
long id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(null, uuid));
finalId = RecipientId.from(id);
}
} else {
if (highTrust) {
Log.i(TAG, "Found out about a UUID for a known E164 user. High-trust, so updating.");
markRegisteredOrThrow(byE164.get(), uuid);
finalId = byE164.get();
} else {
Log.i(TAG, "Found out about a UUID for a known E164 user. Low-trust, so making a new user for the UUID.");
long id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(null, uuid));
finalId = RecipientId.from(id);
}
}
} else {
finalId = byE164.get();
}
} else if (!byE164.isPresent() && byUuid.isPresent()) {
if (e164 != null) {
if (highTrust) {
Log.i(TAG, "Found out about an E164 for a known UUID user. High-trust, so updating.");
setPhoneNumberOrThrow(byUuid.get(), e164);
finalId = byUuid.get();
} else {
Log.i(TAG, "Found out about an E164 for a known UUID user. Low-trust, so doing nothing.");
finalId = byUuid.get();
}
} else {
finalId = byUuid.get();
}
} else {
if (byE164.equals(byUuid)) {
finalId = byUuid.get();
} else {
Log.w(TAG, "Hit a conflict between " + byE164.get() + " (E164) and " + byUuid.get() + " (UUID). They map to different recipients.", new Throwable());
RecipientSettings e164Settings = getRecipientSettings(byE164.get());
if (e164Settings.getUuid() != null) {
if (highTrust) {
Log.w(TAG, "The E164 contact has a different UUID. Likely a case of re-registration. High-trust, so stripping the E164 from the existing account and assigning it to the UUID entry.");
removePhoneNumber(byE164.get(), db);
recipientNeedingRefresh = byE164.get();
setPhoneNumberOrThrow(byUuid.get(), Objects.requireNonNull(e164));
finalId = byUuid.get();
} else {
Log.w(TAG, "The E164 contact has a different UUID. Likely a case of re-registration. Low-trust, so doing nothing.");
finalId = byUuid.get();
}
} else {
if (highTrust) {
Log.w(TAG, "We have one contact with just an E164, and another with UUID. High-trust, so merging the two rows together.");
finalId = merge(byUuid.get(), byE164.get());
recipientNeedingRefresh = byUuid.get();
remapped = new Pair<>(byE164.get(), byUuid.get());
} else {
Log.w(TAG, "We have one contact with just an E164, and another with UUID. Low-trust, so doing nothing.");
finalId = byUuid.get();
}
}
}
}
db.setTransactionSuccessful();
transactionSuccessful = true;
return finalId;
} finally {
db.endTransaction();
if (transactionSuccessful) {
if (recipientNeedingRefresh != null) {
Recipient.live(recipientNeedingRefresh).refresh();
}
if (remapped != null) {
Recipient.live(remapped.first()).refresh(remapped.second());
}
if (recipientNeedingRefresh != null || remapped != null) {
StorageSyncHelper.scheduleSyncForDataChange();
RecipientId.clearCache();
}
}
}
}
private static ContentValues buildContentValuesForNewUser(@Nullable String e164, @Nullable UUID uuid) {
ContentValues values = new ContentValues();
values.put(PHONE, e164);
if (uuid != null) {
values.put(UUID, uuid.toString().toLowerCase());
values.put(REGISTERED, RegisteredState.REGISTERED.getId());
values.put(DIRTY, DirtyState.INSERT.getId());
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
}
return values;
}
public @NonNull RecipientId getOrInsertFromUuid(@NonNull UUID uuid) {
return getOrInsertByColumn(UUID, uuid.toString()).recipientId;
}
@ -423,7 +576,13 @@ public class RecipientDatabase extends Database {
if (cursor != null && cursor.moveToNext()) {
return getRecipientSettings(context, cursor);
} else {
throw new MissingRecipientException(id);
Optional<RecipientId> remapped = RemappedRecords.getInstance().getRecipient(context, id);
if (remapped.isPresent()) {
Log.w(TAG, "Missing recipient, but found it in the remapped records.");
return getRecipientSettings(remapped.get());
} else {
throw new MissingRecipientException(id);
}
}
}
}
@ -527,46 +686,73 @@ public class RecipientDatabase extends Database {
db.beginTransaction();
try {
for (SignalContactRecord insert : contactInserts) {
ContentValues values = validateContactValuesForInsert(getValuesForStorageContact(insert, true));
long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
RecipientId recipientId;
if (id < 0) {
values = validateContactValuesForInsert(getValuesForStorageContact(insert, false));
Log.w(TAG, "Failed to insert! It's likely that these were newly-registered users that were missed in the merge. Doing an update instead.");
if (insert.getAddress().getNumber().isPresent()) {
int count = db.update(TABLE_NAME, values, PHONE + " = ?", new String[] { insert.getAddress().getNumber().get() });
Log.w(TAG, "Updated " + count + " users by E164.");
} else {
int count = db.update(TABLE_NAME, values, UUID + " = ?", new String[] { insert.getAddress().getUuid().get().toString() });
Log.w(TAG, "Updated " + count + " users by UUID.");
}
} else {
RecipientId recipientId = RecipientId.from(id);
if (insert.getIdentityKey().isPresent()) {
try {
IdentityKey identityKey = new IdentityKey(insert.getIdentityKey().get(), 0);
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.getIdentityState()));
} catch (InvalidKeyException e) {
Log.w(TAG, "Failed to process identity key during insert! Skipping.", e);
int count = db.update(TABLE_NAME, values, PHONE + " = ?", new String[] { insert.getAddress().getNumber().get() });
recipientId = getByE164(insert.getAddress().getNumber().get()).get();
Log.w(TAG, "Updated " + count + " users by E164.");
} catch (SQLiteConstraintException e) {
Log.w(TAG, "[applyStorageSyncUpdates -- Insert] Failed to update the UUID on an existing E164 user. Possibly merging.");
recipientId = getAndPossiblyMerge(insert.getAddress().getUuid().get(), insert.getAddress().getNumber().get(), true);
Log.w(TAG, "[applyStorageSyncUpdates -- Insert] Resulting id: " + recipientId);
}
} else {
try {
int count = db.update(TABLE_NAME, values, UUID + " = ?", new String[] { insert.getAddress().getUuid().get().toString() });
recipientId = getByUuid(insert.getAddress().getUuid().get()).get();
Log.w(TAG, "Updated " + count + " users by UUID.");
} catch (SQLiteConstraintException e) {
Log.w(TAG, "[applyStorageSyncUpdates -- Insert] Failed to update the E164 on an existing UUID user. Possibly merging.");
recipientId = getAndPossiblyMerge(insert.getAddress().getUuid().get(), insert.getAddress().getNumber().get(), true);
Log.w(TAG, "[applyStorageSyncUpdates -- Insert] Resulting id: " + recipientId);
}
}
threadDatabase.setArchived(recipientId, insert.isArchived());
needsRefresh.add(recipientId);
} else {
recipientId = RecipientId.from(id);
}
if (insert.getIdentityKey().isPresent()) {
try {
IdentityKey identityKey = new IdentityKey(insert.getIdentityKey().get(), 0);
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.getIdentityState()));
} catch (InvalidKeyException e) {
Log.w(TAG, "Failed to process identity key during insert! Skipping.", e);
}
}
threadDatabase.setArchived(recipientId, insert.isArchived());
needsRefresh.add(recipientId);
}
for (RecordUpdate<SignalContactRecord> update : contactUpdates) {
ContentValues values = getValuesForStorageContact(update.getNew(), false);
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
ContentValues values = getValuesForStorageContact(update.getNew(), false);
if (updateCount < 1) {
throw new AssertionError("Had an update, but it didn't match any rows!");
try {
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
if (updateCount < 1) {
throw new AssertionError("Had an update, but it didn't match any rows!");
}
} catch (SQLiteConstraintException e) {
Log.w(TAG, "[applyStorageSyncUpdates -- Update] Failed to update a user by storageId.");
RecipientId recipientId = getByColumn(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getOld().getId().getRaw())).get();
Log.w(TAG, "[applyStorageSyncUpdates -- Update] Found user " + recipientId + ". Possibly merging.");
recipientId = getAndPossiblyMerge(update.getNew().getAddress().getUuid().orNull(), update.getNew().getAddress().getNumber().orNull(), true);
Log.w(TAG, "[applyStorageSyncUpdates -- Update] Merged into " + recipientId);
db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId));
}
RecipientId recipientId = getByStorageKeyOrThrow(update.getNew().getId().getRaw());
@ -1284,9 +1470,44 @@ public class RecipientDatabase extends Database {
}
}
public void setPhoneNumber(@NonNull RecipientId id, @NonNull String e164) {
/**
* @return True if setting the phone number resulted in changed recipientId, otherwise false.
*/
public boolean setPhoneNumber(@NonNull RecipientId id, @NonNull String e164) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
setPhoneNumberOrThrow(id, e164);
db.setTransactionSuccessful();
return false;
} catch (SQLiteConstraintException e) {
Log.w(TAG, "[setPhoneNumber] Hit a conflict when trying to update " + id + ". Possibly merging.");
RecipientSettings existing = getRecipientSettings(id);
RecipientId newId = getAndPossiblyMerge(existing.getUuid(), e164, true);
Log.w(TAG, "[setPhoneNumber] Resulting id: " + newId);
db.setTransactionSuccessful();
return !newId.equals(existing.getId());
} finally {
db.endTransaction();
}
}
private void removePhoneNumber(@NonNull RecipientId recipientId, @NonNull SQLiteDatabase db) {
ContentValues values = new ContentValues();
values.putNull(PHONE);
db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId));
}
/**
* Should only use if you are confident that this will not result in any contact merging.
*/
public void setPhoneNumberOrThrow(@NonNull RecipientId id, @NonNull String e164) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(PHONE, e164);
if (update(id, contentValues)) {
markDirty(id, DirtyState.UPDATE);
Recipient.live(id).refresh();
@ -1337,13 +1558,38 @@ public class RecipientDatabase extends Database {
return results;
}
public void markRegistered(@NonNull RecipientId id, @Nullable UUID uuid) {
/**
* @return True if setting the UUID resulted in changed recipientId, otherwise false.
*/
public boolean markRegistered(@NonNull RecipientId id, @NonNull UUID uuid) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
markRegisteredOrThrow(id, uuid);
db.setTransactionSuccessful();
return false;
} catch (SQLiteConstraintException e) {
Log.w(TAG, "[markRegistered] Hit a conflict when trying to update " + id + ". Possibly merging.");
RecipientSettings existing = getRecipientSettings(id);
RecipientId newId = getAndPossiblyMerge(uuid, existing.getE164(), true);
Log.w(TAG, "[markRegistered] Merged into " + newId);
db.setTransactionSuccessful();
return !newId.equals(existing.getId());
} finally {
db.endTransaction();
}
}
/**
* Should only use if you are confident that this shouldn't result in any contact merging.
*/
public void markRegisteredOrThrow(@NonNull RecipientId id, @NonNull UUID uuid) {
ContentValues contentValues = new ContentValues(2);
contentValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
if (uuid != null) {
contentValues.put(UUID, uuid.toString().toLowerCase());
}
contentValues.put(UUID, uuid.toString().toLowerCase());
if (update(id, contentValues)) {
markDirty(id, DirtyState.INSERT);
@ -1368,7 +1614,6 @@ public class RecipientDatabase extends Database {
public void markUnregistered(@NonNull RecipientId id) {
ContentValues contentValues = new ContentValues(2);
contentValues.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId());
contentValues.putNull(UUID);
if (update(id, contentValues)) {
markDirty(id, DirtyState.DELETE);
Recipient.live(id).refresh();
@ -1388,8 +1633,16 @@ public class RecipientDatabase extends Database {
values.put(UUID, entry.getValue().toLowerCase());
}
if (update(entry.getKey(), values)) {
markDirty(entry.getKey(), DirtyState.INSERT);
try {
if (update(entry.getKey(), values)) {
markDirty(entry.getKey(), DirtyState.INSERT);
}
} catch (SQLiteConstraintException e) {
Log.w(TAG, "[bulkUpdateRegisteredStatus] Hit a conflict when trying to update " + entry.getKey() + ". Possibly merging.");
RecipientSettings existing = getRecipientSettings(entry.getKey());
RecipientId newId = getAndPossiblyMerge(UuidUtil.parseOrThrow(entry.getValue()), existing.getE164(), true);
Log.w(TAG, "[bulkUpdateRegisteredStatus] Merged into " + newId);
}
}
@ -1449,6 +1702,37 @@ public class RecipientDatabase extends Database {
}
}
public @NonNull Map<RecipientId, String> bulkProcessCdsResult(@NonNull Map<String, UUID> mapping) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
HashMap<RecipientId, String> uuidMap = new HashMap<>();
db.beginTransaction();
try {
for (Map.Entry<String, UUID> entry : mapping.entrySet()) {
String e164 = entry.getKey();
UUID uuid = entry.getValue();
Optional<RecipientId> uuidEntry = uuid != null ? getByUuid(uuid) : Optional.absent();
if (uuidEntry.isPresent()) {
boolean idChanged = setPhoneNumber(uuidEntry.get(), e164);
if (idChanged) {
uuidEntry = getByUuid(Objects.requireNonNull(uuid));
}
}
RecipientId id = uuidEntry.isPresent() ? uuidEntry.get() : getOrInsertFromE164(e164);
uuidMap.put(id, uuid != null ? uuid.toString() : null);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return uuidMap;
}
public @NonNull List<RecipientId> getUninvitedRecipientsForInsights() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
List<RecipientId> results = new LinkedList<>();
@ -1745,7 +2029,13 @@ public class RecipientDatabase extends Database {
values.put(DIRTY, DirtyState.CLEAN.getId());
for (RecipientId id : recipients) {
db.update(TABLE_NAME, values, ID_WHERE, new String[]{ id.serialize() });
Optional<RecipientId> remapped = RemappedRecords.getInstance().getRecipient(context, id);
if (remapped.isPresent()) {
Log.w(TAG, "While clearing dirty state, noticed we have a remapped contact (" + id + " to " + remapped.get() + "). Safe to delete now.");
db.delete(TABLE_NAME, ID_WHERE, new String[]{id.serialize()});
} else {
db.update(TABLE_NAME, values, ID_WHERE, new String[]{id.serialize()});
}
}
db.setTransactionSuccessful();
@ -1858,6 +2148,136 @@ public class RecipientDatabase extends Database {
}
}
/**
* Merges one UUID recipient with an E164 recipient. It is assumed that the E164 recipient does
* *not* have a UUID.
*/
@SuppressWarnings("ConstantConditions")
private @NonNull RecipientId merge(@NonNull RecipientId byUuid, @NonNull RecipientId byE164) {
ensureInTransaction();
SQLiteDatabase db = databaseHelper.getWritableDatabase();
RecipientSettings uuidSettings = getRecipientSettings(byUuid);
RecipientSettings e164Settings = getRecipientSettings(byE164);
// Recipient
if (e164Settings.getStorageId() == null) {
Log.w(TAG, "No storageId on the E164 recipient. Can delete right away.");
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164));
} else {
Log.w(TAG, "The E164 recipient has a storageId. Clearing data and marking for deletion.");
ContentValues values = new ContentValues();
values.putNull(PHONE);
values.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId());
values.put(DIRTY, DirtyState.DELETE.getId());
db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(byE164));
}
RemappedRecords.getInstance().addRecipient(context, byE164, byUuid);
ContentValues uuidValues = new ContentValues();
uuidValues.put(PHONE, e164Settings.getE164());
uuidValues.put(BLOCKED, e164Settings.isBlocked() || uuidSettings.isBlocked());
uuidValues.put(MESSAGE_RINGTONE, Optional.fromNullable(uuidSettings.getMessageRingtone()).or(Optional.fromNullable(e164Settings.getMessageRingtone())).transform(Uri::toString).orNull());
uuidValues.put(MESSAGE_VIBRATE, uuidSettings.getMessageVibrateState() != VibrateState.DEFAULT ? uuidSettings.getMessageVibrateState().getId() : e164Settings.getMessageVibrateState().getId());
uuidValues.put(CALL_RINGTONE, Optional.fromNullable(uuidSettings.getCallRingtone()).or(Optional.fromNullable(e164Settings.getCallRingtone())).transform(Uri::toString).orNull());
uuidValues.put(CALL_VIBRATE, uuidSettings.getCallVibrateState() != VibrateState.DEFAULT ? uuidSettings.getCallVibrateState().getId() : e164Settings.getCallVibrateState().getId());
uuidValues.put(NOTIFICATION_CHANNEL, uuidSettings.getNotificationChannel() != null ? uuidSettings.getNotificationChannel() : e164Settings.getNotificationChannel());
uuidValues.put(MUTE_UNTIL, uuidSettings.getMuteUntil() > 0 ? uuidSettings.getMuteUntil() : e164Settings.getMuteUntil());
uuidValues.put(COLOR, Optional.fromNullable(uuidSettings.getColor()).or(Optional.fromNullable(e164Settings.getColor())).transform(MaterialColor::serialize).orNull());
uuidValues.put(SEEN_INVITE_REMINDER, e164Settings.getInsightsBannerTier().getId());
uuidValues.put(DEFAULT_SUBSCRIPTION_ID, e164Settings.getDefaultSubscriptionId().or(-1));
uuidValues.put(MESSAGE_EXPIRATION_TIME, uuidSettings.getExpireMessages() > 0 ? uuidSettings.getExpireMessages() : e164Settings.getExpireMessages());
uuidValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
uuidValues.put(SYSTEM_DISPLAY_NAME, e164Settings.getSystemDisplayName());
uuidValues.put(SYSTEM_PHOTO_URI, e164Settings.getSystemContactPhotoUri());
uuidValues.put(SYSTEM_PHONE_LABEL, e164Settings.getSystemPhoneLabel());
uuidValues.put(SYSTEM_CONTACT_URI, e164Settings.getSystemContactUri());
uuidValues.put(PROFILE_SHARING, uuidSettings.isProfileSharing() || e164Settings.isProfileSharing());
uuidValues.put(GROUPS_V2_CAPABILITY, uuidSettings.getGroupsV2Capability() != Recipient.Capability.UNKNOWN ? uuidSettings.getGroupsV2Capability().serialize() : e164Settings.getGroupsV2Capability().serialize());
if (uuidSettings.getProfileKey() != null) {
updateProfileValuesForMerge(uuidValues, uuidSettings);
} else if (e164Settings.getProfileKey() != null) {
updateProfileValuesForMerge(uuidValues, e164Settings);
}
db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(byUuid));
// Identities
db.delete(IdentityDatabase.TABLE_NAME, IdentityDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164));
// Group Receipts
ContentValues groupReceiptValues = new ContentValues();
groupReceiptValues.put(GroupReceiptDatabase.RECIPIENT_ID, byUuid.serialize());
db.update(GroupReceiptDatabase.TABLE_NAME, groupReceiptValues, GroupReceiptDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164));
// Groups
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
for (GroupDatabase.GroupRecord group : groupDatabase.getGroupsContainingMember(byE164, false)) {
List<RecipientId> newMembers = new ArrayList<>(group.getMembers());
newMembers.remove(byE164);
ContentValues groupValues = new ContentValues();
groupValues.put(GroupDatabase.MEMBERS, RecipientId.toSerializedList(newMembers));
db.update(GroupDatabase.TABLE_NAME, groupValues, GroupDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(group.getRecipientId()));
}
// Threads
ThreadDatabase.MergeResult threadMerge = DatabaseFactory.getThreadDatabase(context).merge(byUuid, byE164);
// SMS Messages
ContentValues smsValues = new ContentValues();
smsValues.put(SmsDatabase.RECIPIENT_ID, byUuid.serialize());
if (threadMerge.neededMerge) {
smsValues.put(SmsDatabase.THREAD_ID, threadMerge.threadId);
}
db.update(SmsDatabase.TABLE_NAME, smsValues, SmsDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164));
// MMS Messages
ContentValues mmsValues = new ContentValues();
mmsValues.put(MmsDatabase.RECIPIENT_ID, byUuid.serialize());
if (threadMerge.neededMerge) {
mmsValues.put(MmsDatabase.THREAD_ID, threadMerge.threadId);
}
db.update(MmsDatabase.TABLE_NAME, mmsValues, MmsDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164));
// Sessions
boolean hasE164Session = DatabaseFactory.getSessionDatabase(context).getAllFor(byE164).size() > 0;
boolean hasUuidSession = DatabaseFactory.getSessionDatabase(context).getAllFor(byUuid).size() > 0;
if (hasE164Session && hasUuidSession) {
Log.w(TAG, "Had a session for both users. Deleting the E164.");
db.delete(SessionDatabase.TABLE_NAME, SessionDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164));
} else if (hasE164Session && !hasUuidSession) {
Log.w(TAG, "Had a session for E164, but not UUID. Re-assigning to the UUID.");
ContentValues values = new ContentValues();
values.put(SessionDatabase.RECIPIENT_ID, byUuid.serialize());
db.update(SessionDatabase.TABLE_NAME, values, SessionDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164));
} else if (!hasE164Session && hasUuidSession) {
Log.w(TAG, "Had a session for UUID, but not E164. No action necessary.");
} else {
Log.w(TAG, "Had no sessions. No action necessary.");
}
DatabaseFactory.getThreadDatabase(context).update(threadMerge.threadId, false, false);
return byUuid;
}
private static void updateProfileValuesForMerge(@NonNull ContentValues values, @NonNull RecipientSettings settings) {
values.put(PROFILE_KEY, settings.getProfileKey() != null ? Base64.encodeBytes(settings.getProfileKey()) : null);
values.put(PROFILE_KEY_CREDENTIAL, settings.getProfileKeyCredential() != null ? Base64.encodeBytes(settings.getProfileKeyCredential()) : null);
values.put(SIGNAL_PROFILE_AVATAR, settings.getProfileAvatar());
values.put(PROFILE_GIVEN_NAME, settings.getProfileName().getGivenName());
values.put(PROFILE_FAMILY_NAME, settings.getProfileName().getFamilyName());
values.put(PROFILE_JOINED_NAME, settings.getProfileName().toString());
}
private void ensureInTransaction() {
if (!databaseHelper.getWritableDatabase().inTransaction()) {
throw new IllegalStateException("Must be in a transaction!");
}
}
public class BulkOperationsHandle {
private final SQLiteDatabase database;
@ -2245,6 +2665,24 @@ public class RecipientDatabase extends Database {
}
}
public final static class RecipientIdResult {
private final RecipientId recipientId;
private final boolean requiresDirectoryRefresh;
public RecipientIdResult(@NonNull RecipientId recipientId, boolean requiresDirectoryRefresh) {
this.recipientId = recipientId;
this.requiresDirectoryRefresh = requiresDirectoryRefresh;
}
public @NonNull RecipientId getRecipientId() {
return recipientId;
}
public boolean requiresDirectoryRefresh() {
return requiresDirectoryRefresh;
}
}
private static class PendingContactInfo {
private final String displayName;

View File

@ -0,0 +1,89 @@
package org.thoughtcrime.securesms.database;
import android.content.Context;
import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Merging together recipients and threads is messy business. We can easily replace *almost* all of
* the references, but there are specific places (notably reactions, jobs, etc) that are really
* expensive to address. For these cases, we keep mappings of old IDs to new ones to use as a
* fallback.
*
* There should be very few of these, so we keep them in a fast, lazily-loaded memory cache.
*
* One important thing to note is that this class will often be accesses inside of database
* transactions. As a result, it cannot attempt to acquire a database lock while holding a
* separate lock. Instead, we use the database lock itself as a locking mechanism.
*/
class RemappedRecords {
private static final RemappedRecords INSTANCE = new RemappedRecords();
private Map<RecipientId, RecipientId> recipientMap;
private Map<Long, Long> threadMap;
private RemappedRecords() {}
static RemappedRecords getInstance() {
return INSTANCE;
}
@NonNull Optional<RecipientId> getRecipient(@NonNull Context context, @NonNull RecipientId oldId) {
ensureRecipientMapIsPopulated(context);
return Optional.fromNullable(recipientMap.get(oldId));
}
@NonNull Optional<Long> getThread(@NonNull Context context, long oldId) {
ensureThreadMapIsPopulated(context);
return Optional.fromNullable(threadMap.get(oldId));
}
/**
* Can only be called inside of a transaction.
*/
void addRecipient(@NonNull Context context, @NonNull RecipientId oldId, @NonNull RecipientId newId) {
ensureInTransaction(context);
ensureRecipientMapIsPopulated(context);
recipientMap.put(oldId, newId);
DatabaseFactory.getRemappedRecordsDatabase(context).addRecipientMapping(oldId, newId);
}
/**
* Can only be called inside of a transaction.
*/
void addThread(@NonNull Context context, long oldId, long newId) {
ensureInTransaction(context);
ensureRecipientMapIsPopulated(context);
threadMap.put(oldId, newId);
DatabaseFactory.getRemappedRecordsDatabase(context).addThreadMapping(oldId, newId);
}
private void ensureRecipientMapIsPopulated(@NonNull Context context) {
if (recipientMap == null) {
recipientMap = DatabaseFactory.getRemappedRecordsDatabase(context).getAllRecipientMappings();
}
}
private void ensureThreadMapIsPopulated(@NonNull Context context) {
if (threadMap == null) {
threadMap = DatabaseFactory.getRemappedRecordsDatabase(context).getAllThreadMappings();
}
}
private void ensureInTransaction(@NonNull Context context) {
if (!DatabaseFactory.inTransaction(context)) {
throw new IllegalStateException("Must be in a transaction!");
}
}
}

View File

@ -0,0 +1,141 @@
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import androidx.annotation.NonNull;
import net.sqlcipher.Cursor;
import net.sqlcipher.database.SQLiteDatabase;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CursorUtil;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* The backing datastore for {@link RemappedRecords}. See that class for more details.
*/
public class RemappedRecordsDatabase extends Database {
public static final String[] CREATE_TABLE = { Recipients.CREATE_TABLE,
Threads.CREATE_TABLE };
private static class SharedColumns {
protected static final String ID = "_id";
protected static final String OLD_ID = "old_id";
protected static final String NEW_ID = "new_id";
}
private static final class Recipients extends SharedColumns {
private static final String TABLE_NAME = "remapped_recipients";
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
OLD_ID + " INTEGER UNIQUE, " +
NEW_ID + " INTEGER)";
}
private static final class Threads extends SharedColumns {
private static final String TABLE_NAME = "remapped_threads";
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
OLD_ID + " INTEGER UNIQUE, " +
NEW_ID + " INTEGER)";
}
RemappedRecordsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
}
@NonNull Map<RecipientId, RecipientId> getAllRecipientMappings() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Map<RecipientId, RecipientId> recipientMap = new HashMap<>();
db.beginTransaction();
try {
List<Mapping> mappings = getAllMappings(Recipients.TABLE_NAME);
for (Mapping mapping : mappings) {
RecipientId oldId = RecipientId.from(mapping.getOldId());
RecipientId newId = RecipientId.from(mapping.getNewId());
recipientMap.put(oldId, newId);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return recipientMap;
}
@NonNull Map<Long, Long> getAllThreadMappings() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Map<Long, Long> threadMap = new HashMap<>();
db.beginTransaction();
try {
List<Mapping> mappings = getAllMappings(Threads.TABLE_NAME);
for (Mapping mapping : mappings) {
threadMap.put(mapping.getOldId(), mapping.getNewId());
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return threadMap;
}
void addRecipientMapping(@NonNull RecipientId oldId, @NonNull RecipientId newId) {
addMapping(Recipients.TABLE_NAME, new Mapping(oldId.toLong(), newId.toLong()));
}
void addThreadMapping(long oldId, long newId) {
addMapping(Threads.TABLE_NAME, new Mapping(oldId, newId));
}
private @NonNull List<Mapping> getAllMappings(@NonNull String table) {
List<Mapping> mappings = new LinkedList<>();
try (Cursor cursor = databaseHelper.getReadableDatabase().query(table, null, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
long oldId = CursorUtil.requireLong(cursor, SharedColumns.OLD_ID);
long newId = CursorUtil.requireLong(cursor, SharedColumns.NEW_ID);
mappings.add(new Mapping(oldId, newId));
}
}
return mappings;
}
private void addMapping(@NonNull String table, @NonNull Mapping mapping) {
ContentValues values = new ContentValues();
values.put(SharedColumns.OLD_ID, mapping.getOldId());
values.put(SharedColumns.NEW_ID, mapping.getNewId());
databaseHelper.getWritableDatabase().insert(table, null, values);
}
static final class Mapping {
private final long oldId;
private final long newId;
public Mapping(long oldId, long newId) {
this.oldId = oldId;
this.newId = newId;
}
public long getOldId() {
return oldId;
}
public long getNewId() {
return newId;
}
}
}

View File

@ -22,6 +22,7 @@ import android.content.Context;
import android.database.Cursor;
import android.database.MergeCursor;
import android.net.Uri;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -49,6 +50,7 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
@ -729,6 +731,19 @@ public class ThreadDatabase extends Database {
}
}
public long getOrCreateValidThreadId(@NonNull Recipient recipient, long candidateId) {
return getOrCreateValidThreadId(recipient, candidateId, DistributionTypes.DEFAULT);
}
public long getOrCreateValidThreadId(@NonNull Recipient recipient, long candidateId, int distributionType) {
if (candidateId != -1) {
Optional<Long> remapped = RemappedRecords.getInstance().getThread(context, candidateId);
return remapped.isPresent() ? remapped.get() : candidateId;
} else {
return getThreadIdFor(recipient, distributionType);
}
}
public long getThreadIdFor(@NonNull Recipient recipient) {
return getThreadIdFor(recipient, DistributionTypes.DEFAULT);
}
@ -742,7 +757,7 @@ public class ThreadDatabase extends Database {
}
}
public Long getThreadIdFor(@NonNull RecipientId recipientId) {
public @Nullable Long getThreadIdFor(@NonNull RecipientId recipientId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String where = RECIPIENT_ID + " = ?";
String[] recipientsArg = new String[]{recipientId.serialize()};
@ -799,12 +814,18 @@ public class ThreadDatabase extends Database {
}
public boolean update(long threadId, boolean unarchive) {
return update(threadId, unarchive, true);
}
public boolean update(long threadId, boolean unarchive, boolean allowDeletion) {
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
long count = mmsSmsDatabase.getConversationCount(threadId);
if (count == 0) {
deleteThread(threadId);
notifyConversationListListeners();
if (allowDeletion) {
deleteThread(threadId);
notifyConversationListListeners();
}
return true;
}
@ -832,6 +853,81 @@ public class ThreadDatabase extends Database {
}
}
@NonNull MergeResult merge(@NonNull RecipientId primaryRecipientId, @NonNull RecipientId secondaryRecipientId) {
if (!databaseHelper.getWritableDatabase().inTransaction()) {
throw new IllegalStateException("Must be in a transaction!");
}
Log.w(TAG, "Merging threads. Primary: " + primaryRecipientId + ", Secondary: " + secondaryRecipientId);
ThreadRecord primary = getThreadRecord(getThreadIdFor(primaryRecipientId));
ThreadRecord secondary = getThreadRecord(getThreadIdFor(secondaryRecipientId));
if (primary != null && secondary == null) {
Log.w(TAG, "[merge] Only had a thread for primary. Returning that.");
return new MergeResult(primary.getThreadId(), false);
} else if (primary == null && secondary != null) {
Log.w(TAG, "[merge] Only had a thread for secondary. Updating it to have the recipientId of the primary.");
ContentValues values = new ContentValues();
values.put(RECIPIENT_ID, primaryRecipientId.serialize());
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(secondary.getThreadId()));
return new MergeResult(secondary.getThreadId(), false);
} else if (primary == null && secondary == null) {
Log.w(TAG, "[merge] No thread for either.");
return new MergeResult(-1, false);
} else {
Log.w(TAG, "[merge] Had a thread for both. Deleting the secondary and merging the attributes together.");
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(secondary.getThreadId()));
if (primary.getExpiresIn() != secondary.getExpiresIn()) {
ContentValues values = new ContentValues();
if (primary.getExpiresIn() == 0) {
values.put(EXPIRES_IN, secondary.getExpiresIn());
} else if (secondary.getExpiresIn() == 0) {
values.put(EXPIRES_IN, primary.getExpiresIn());
} else {
values.put(EXPIRES_IN, Math.min(primary.getExpiresIn(), secondary.getExpiresIn()));
}
db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(primary.getThreadId()));
}
ContentValues draftValues = new ContentValues();
draftValues.put(DraftDatabase.THREAD_ID, primary.getThreadId());
db.update(DraftDatabase.TABLE_NAME, draftValues, DraftDatabase.THREAD_ID + " = ?", SqlUtil.buildArgs(secondary.getThreadId()));
ContentValues searchValues = new ContentValues();
searchValues.put(SearchDatabase.THREAD_ID, primary.getThreadId());
db.update(SearchDatabase.SMS_FTS_TABLE_NAME, searchValues, SearchDatabase.THREAD_ID + " = ?", SqlUtil.buildArgs(secondary.getThreadId()));
db.update(SearchDatabase.MMS_FTS_TABLE_NAME, searchValues, SearchDatabase.THREAD_ID + " = ?", SqlUtil.buildArgs(secondary.getThreadId()));
RemappedRecords.getInstance().addThread(context, secondary.getThreadId(), primary.getThreadId());
return new MergeResult(primary.getThreadId(), true);
}
}
private @Nullable ThreadRecord getThreadRecord(@Nullable Long threadId) {
if (threadId == null) {
return null;
}
String query = createQuery(TABLE_NAME + "." + ID + " = ?", 1);
try (Cursor cursor = databaseHelper.getReadableDatabase().rawQuery(query, SqlUtil.buildArgs(threadId))) {
if (cursor != null && cursor.moveToFirst()) {
return readerFor(cursor).getCurrent();
}
}
return null;
}
private @Nullable Uri getAttachmentUriFor(MessageRecord record) {
if (!record.isMms() || record.isMmsNotification() || record.isGroupAction()) return null;
@ -1164,4 +1260,14 @@ public class ThreadDatabase extends Database {
return lastScrolled;
}
}
static final class MergeResult {
final long threadId;
final boolean neededMerge;
private MergeResult(long threadId, boolean neededMerge) {
this.threadId = threadId;
this.neededMerge = neededMerge;
}
}
}

View File

@ -22,6 +22,7 @@ import net.sqlcipher.database.SQLiteDatabaseHook;
import net.sqlcipher.database.SQLiteOpenHelper;
import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy;
import org.thoughtcrime.securesms.database.RemappedRecordsDatabase;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.RecipientId;
@ -137,8 +138,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int SERVER_DELIVERED_TIMESTAMP = 64;
private static final int QUOTE_CLEANUP = 65;
private static final int BORDERLESS = 66;
private static final int REMAPPED_RECORDS = 67;
private static final int DATABASE_VERSION = 66;
private static final int DATABASE_VERSION = 67;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@ -184,6 +186,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(MegaphoneDatabase.CREATE_TABLE);
executeStatements(db, SearchDatabase.CREATE_TABLE);
executeStatements(db, JobDatabase.CREATE_TABLE);
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE);
executeStatements(db, RecipientDatabase.CREATE_INDEXS);
executeStatements(db, SmsDatabase.CREATE_INDEXS);
@ -958,6 +961,15 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE part ADD COLUMN borderless INTEGER DEFAULT 0");
}
if (oldVersion < REMAPPED_RECORDS) {
db.execSQL("CREATE TABLE remapped_recipients (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"old_id INTEGER UNIQUE, " +
"new_id INTEGER)");
db.execSQL("CREATE TABLE remapped_threads (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"old_id INTEGER UNIQUE, " +
"new_id INTEGER)");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@ -86,7 +86,7 @@ public final class GroupProtoUtil {
return Recipient.UNKNOWN;
}
return Recipient.externalPush(context, uuid, null);
return Recipient.externalPush(context, uuid, null, false);
}
@WorkerThread

View File

@ -108,7 +108,7 @@ public final class GroupV1MessageProcessor {
database.create(id, group.getName().orNull(), members,
avatar != null && avatar.isPointer() ? avatar.asPointer() : null, null);
Recipient sender = Recipient.externalPush(context, content.getSender());
Recipient sender = Recipient.externalHighTrustPush(context, content.getSender());
if (sender.isSystemContact() || sender.isProfileSharing()) {
Log.i(TAG, "Auto-enabling profile sharing because 'adder' is trusted. contact: " + sender.isSystemContact() + ", profileSharing: " + sender.isProfileSharing());
@ -186,7 +186,7 @@ public final class GroupV1MessageProcessor {
@NonNull SignalServiceContent content,
@NonNull GroupRecord record)
{
Recipient sender = Recipient.externalPush(context, content.getSender());
Recipient sender = Recipient.externalHighTrustPush(context, content.getSender());
if (record.getMembers().contains(sender.getId())) {
ApplicationDependencies.getJobManager().add(new PushGroupUpdateJob(sender.getId(), record.getId()));
@ -208,8 +208,10 @@ public final class GroupV1MessageProcessor {
GroupContext.Builder builder = createGroupContext(group);
builder.setType(GroupContext.Type.QUIT);
if (members.contains(Recipient.externalPush(context, content.getSender()).getId())) {
database.remove(id, Recipient.externalPush(context, content.getSender()).getId());
RecipientId senderId = RecipientId.fromHighTrust(content.getSender());
if (members.contains(senderId)) {
database.remove(id, senderId);
if (outgoing) database.setActive(id, false);
return storeMessage(context, content, group, builder.build(), outgoing);
@ -245,7 +247,7 @@ public final class GroupV1MessageProcessor {
} else {
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
String body = Base64.encodeBytes(storage.toByteArray());
IncomingTextMessage incoming = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), content.getServerReceivedTimestamp(), body, Optional.of(GroupId.v1orThrow(group.getGroupId())), 0, content.isNeedsReceipt());
IncomingTextMessage incoming = new IncomingTextMessage(Recipient.externalHighTrustPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), content.getServerReceivedTimestamp(), body, Optional.of(GroupId.v1orThrow(group.getGroupId())), 0, content.isNeedsReceipt());
IncomingGroupUpdateMessage groupMessage = new IncomingGroupUpdateMessage(incoming, storage, body);
Optional<InsertResult> insertResult = smsDatabase.insertMessageInbox(groupMessage);

View File

@ -251,7 +251,7 @@ public final class PushProcessMessageJob extends BaseJob {
Log.w(TAG, "Bad groupId! Using default queue.");
}
} else {
queueSuffix = RecipientId.from(content.getSender()).toQueueKey();
queueSuffix = RecipientId.fromHighTrust(content.getSender()).toQueueKey();
}
} else if (exceptionMetadata != null) {
Recipient recipient = exceptionMetadata.groupId != null ? Recipient.externalGroup(context, exceptionMetadata.groupId)
@ -498,7 +498,7 @@ public final class PushProcessMessageJob extends BaseJob {
database.markAsMissedCall(smsMessageId.get());
} else {
Intent intent = new Intent(context, WebRtcCallService.class);
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
RemotePeer remotePeer = new RemotePeer(Recipient.externalHighTrustPush(context, content.getSender()).getId());
intent.setAction(WebRtcCallService.ACTION_RECEIVE_OFFER)
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
@ -520,7 +520,7 @@ public final class PushProcessMessageJob extends BaseJob {
{
Log.i(TAG, "handleCallAnswerMessage...");
Intent intent = new Intent(context, WebRtcCallService.class);
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
RemotePeer remotePeer = new RemotePeer(Recipient.externalHighTrustPush(context, content.getSender()).getId());
intent.setAction(WebRtcCallService.ACTION_RECEIVE_ANSWER)
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
@ -545,7 +545,7 @@ public final class PushProcessMessageJob extends BaseJob {
}
Intent intent = new Intent(context, WebRtcCallService.class);
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
RemotePeer remotePeer = new RemotePeer(Recipient.externalHighTrustPush(context, content.getSender()).getId());
intent.setAction(WebRtcCallService.ACTION_RECEIVE_ICE_CANDIDATES)
.putExtra(WebRtcCallService.EXTRA_CALL_ID, callId)
@ -565,7 +565,7 @@ public final class PushProcessMessageJob extends BaseJob {
DatabaseFactory.getSmsDatabase(context).markAsMissedCall(smsMessageId.get());
} else {
Intent intent = new Intent(context, WebRtcCallService.class);
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
RemotePeer remotePeer = new RemotePeer(Recipient.externalHighTrustPush(context, content.getSender()).getId());
intent.setAction(WebRtcCallService.ACTION_RECEIVE_HANGUP)
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
@ -585,7 +585,7 @@ public final class PushProcessMessageJob extends BaseJob {
Log.i(TAG, "handleCallBusyMessage");
Intent intent = new Intent(context, WebRtcCallService.class);
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
RemotePeer remotePeer = new RemotePeer(Recipient.externalHighTrustPush(context, content.getSender()).getId());
intent.setAction(WebRtcCallService.ACTION_RECEIVE_BUSY)
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
@ -599,7 +599,7 @@ public final class PushProcessMessageJob extends BaseJob {
@NonNull Optional<Long> smsMessageId)
{
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(),
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(Recipient.externalHighTrustPush(context, content.getSender()).getId(),
content.getSenderDevice(),
content.getTimestamp(),
content.getServerReceivedTimestamp(),
@ -677,7 +677,7 @@ public final class PushProcessMessageJob extends BaseJob {
if (group.getGroupV1().isPresent()) {
SignalServiceGroup groupV1 = group.getGroupV1().get();
if (groupV1.getType() != SignalServiceGroup.Type.REQUEST_INFO) {
ApplicationDependencies.getJobManager().add(new RequestGroupInfoJob(Recipient.externalPush(context, content.getSender()).getId(), GroupId.v1(groupV1.getGroupId())));
ApplicationDependencies.getJobManager().add(new RequestGroupInfoJob(Recipient.externalHighTrustPush(context, content.getSender()).getId(), GroupId.v1(groupV1.getGroupId())));
} else {
Log.w(TAG, "Received a REQUEST_INFO message for a group we don't know about. Ignoring.");
}
@ -708,7 +708,7 @@ public final class PushProcessMessageJob extends BaseJob {
try {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Recipient sender = Recipient.externalPush(context, content.getSender());
Recipient sender = Recipient.externalHighTrustPush(context, content.getSender());
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender.getId(),
content.getTimestamp(),
content.getServerReceivedTimestamp(),
@ -744,7 +744,7 @@ public final class PushProcessMessageJob extends BaseJob {
MessageRecord targetMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(reaction.getTargetSentTimestamp(), targetAuthor.getId());
if (targetMessage != null && !targetMessage.isRemoteDelete()) {
Recipient reactionAuthor = Recipient.externalPush(context, content.getSender());
Recipient reactionAuthor = Recipient.externalHighTrustPush(context, content.getSender());
MessagingDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
if (reaction.isRemove()) {
@ -766,7 +766,7 @@ public final class PushProcessMessageJob extends BaseJob {
private void handleRemoteDelete(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
SignalServiceDataMessage.RemoteDelete delete = message.getRemoteDelete().get();
Recipient sender = Recipient.externalPush(context, content.getSender());
Recipient sender = Recipient.externalHighTrustPush(context, content.getSender());
MessageRecord targetMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(delete.getTargetSentTimestamp(), sender.getId());
if (targetMessage != null && RemoteDeleteUtil.isValidReceive(targetMessage, sender, content.getServerReceivedTimestamp())) {
@ -1024,7 +1024,7 @@ public final class PushProcessMessageJob extends BaseJob {
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
Optional<Attachment> sticker = getStickerAttachment(message.getSticker());
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Recipient.externalPush(context, content.getSender()).getId(),
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(RecipientId.fromHighTrust(content.getSender()),
message.getTimestamp(),
content.getServerReceivedTimestamp(),
-1,
@ -1243,7 +1243,7 @@ public final class PushProcessMessageJob extends BaseJob {
} else {
notifyTypingStoppedFromIncomingMessage(recipient, content.getSender(), content.getSenderDevice());
IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(),
IncomingTextMessage textMessage = new IncomingTextMessage(RecipientId.fromHighTrust(content.getSender()),
content.getSenderDevice(),
message.getTimestamp(),
content.getServerReceivedTimestamp(),
@ -1445,7 +1445,7 @@ public final class PushProcessMessageJob extends BaseJob {
@NonNull byte[] messageProfileKeyBytes)
{
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
Recipient recipient = Recipient.externalPush(context, content.getSender());
Recipient recipient = Recipient.externalHighTrustPush(context, content.getSender());
ProfileKey messageProfileKey = ProfileKeyUtil.profileKeyOrNull(messageProfileKeyBytes);
if (messageProfileKey != null) {
@ -1460,7 +1460,7 @@ public final class PushProcessMessageJob extends BaseJob {
private void handleNeedsDeliveryReceipt(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message)
{
ApplicationDependencies.getJobManager().add(new SendDeliveryReceiptJob(Recipient.externalPush(context, content.getSender()).getId(), message.getTimestamp()));
ApplicationDependencies.getJobManager().add(new SendDeliveryReceiptJob(RecipientId.fromHighTrust(content.getSender()), message.getTimestamp()));
}
@SuppressLint("DefaultLocale")
@ -1470,7 +1470,7 @@ public final class PushProcessMessageJob extends BaseJob {
for (long timestamp : message.getTimestamps()) {
Log.i(TAG, String.format("Received encrypted delivery receipt: (XXXXX, %d)", timestamp));
DatabaseFactory.getMmsSmsDatabase(context)
.incrementDeliveryReceiptCount(new SyncMessageId(Recipient.externalPush(context, content.getSender()).getId(), timestamp), System.currentTimeMillis());
.incrementDeliveryReceiptCount(new SyncMessageId(RecipientId.fromHighTrust(content.getSender()), timestamp), System.currentTimeMillis());
}
}
@ -1482,7 +1482,7 @@ public final class PushProcessMessageJob extends BaseJob {
for (long timestamp : message.getTimestamps()) {
Log.i(TAG, String.format("Received encrypted read receipt: (XXXXX, %d)", timestamp));
Recipient sender = Recipient.externalPush(context, content.getSender());
Recipient sender = Recipient.externalHighTrustPush(context, content.getSender());
SyncMessageId id = new SyncMessageId(sender.getId(), timestamp);
boolean handled = DatabaseFactory.getMmsSmsDatabase(context)
.incrementReadReceiptCount(id, content.getTimestamp());
@ -1503,7 +1503,7 @@ public final class PushProcessMessageJob extends BaseJob {
return;
}
Recipient author = Recipient.externalPush(context, content.getSender());
Recipient author = Recipient.externalHighTrustPush(context, content.getSender());
long threadId;
@ -1703,7 +1703,7 @@ public final class PushProcessMessageJob extends BaseJob {
@NonNull SignalServiceDataMessage message)
throws BadGroupIdException
{
return getGroupRecipient(message.getGroupContext()).or(() -> Recipient.externalPush(context, content.getSender()));
return getGroupRecipient(message.getGroupContext()).or(() -> Recipient.externalHighTrustPush(context, content.getSender()));
}
private Recipient getMessageDestination(@NonNull SignalServiceContent content,
@ -1740,7 +1740,7 @@ public final class PushProcessMessageJob extends BaseJob {
return true;
}
Recipient sender = Recipient.externalPush(context, content.getSender());
Recipient sender = Recipient.externalHighTrustPush(context, content.getSender());
if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();

View File

@ -77,7 +77,7 @@ public class IncomingMessageProcessor {
*/
public @Nullable String processEnvelope(@NonNull SignalServiceEnvelope envelope) {
if (envelope.hasSource()) {
Recipient.externalPush(context, envelope.getSourceAddress());
Recipient.externalHighTrustPush(context, envelope.getSourceAddress());
}
if (envelope.isReceipt()) {
@ -110,7 +110,7 @@ public class IncomingMessageProcessor {
private void processReceipt(@NonNull SignalServiceEnvelope envelope) {
Log.i(TAG, String.format(Locale.ENGLISH, "Received receipt: (XXXXX, %d)", envelope.getTimestamp()));
mmsSmsDatabase.incrementDeliveryReceiptCount(new SyncMessageId(Recipient.externalPush(context, envelope.getSourceAddress()).getId(), envelope.getTimestamp()),
mmsSmsDatabase.incrementDeliveryReceiptCount(new SyncMessageId(Recipient.externalHighTrustPush(context, envelope.getSourceAddress()).getId(), envelope.getTimestamp()),
System.currentTimeMillis());
}

View File

@ -74,7 +74,7 @@ public class UuidMigrationJob extends MigrationJob {
RecipientId self = Recipient.self().getId();
UUID localUuid = ApplicationDependencies.getSignalServiceAccountManager().getOwnUuid();
DatabaseFactory.getRecipientDatabase(context).markRegistered(self, localUuid);
DatabaseFactory.getRecipientDatabase(context).markRegisteredOrThrow(self, localUuid);
TextSecurePreferences.setLocalUuid(context, localUuid);
}

View File

@ -142,18 +142,27 @@ public final class LiveRecipient {
return updated;
}
@WorkerThread
public void refresh() {
refresh(getId());
}
/**
* Forces a reload of the underlying recipient.
*/
@WorkerThread
public void refresh() {
public void refresh(@NonNull RecipientId id) {
if (!getId().equals(id)) {
Log.w(TAG, "Switching ID from " + getId() + " to " + id);
}
if (getId().isUnknown()) return;
if (Util.isMainThread()) {
Log.w(TAG, "[Refresh][MAIN] " + getId(), new Throwable());
Log.w(TAG, "[Refresh][MAIN] " + id, new Throwable());
}
Recipient recipient = fetchAndCacheRecipientFromDisk(getId());
Recipient recipient = fetchAndCacheRecipientFromDisk(id);
List<Recipient> participants = Stream.of(recipient.getParticipants())
.map(Recipient::getId)
.map(this::fetchAndCacheRecipientFromDisk)

View File

@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.contacts.avatars.TransparentContactPhoto;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientIdResult;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
@ -129,7 +130,7 @@ public class Recipient {
*/
@WorkerThread
public static @NonNull Recipient externalUsername(@NonNull Context context, @NonNull UUID uuid, @NonNull String username) {
Recipient recipient = externalPush(context, uuid, null);
Recipient recipient = externalPush(context, uuid, null, false);
DatabaseFactory.getRecipientDatabase(context).setUsername(recipient.getId(), username);
return recipient;
}
@ -137,11 +138,25 @@ public class Recipient {
/**
* Returns a fully-populated {@link Recipient} based off of a {@link SignalServiceAddress},
* creating one in the database if necessary. Convenience overload of
* {@link #externalPush(Context, UUID, String)}
* {@link #externalPush(Context, UUID, String, boolean)}
*/
@WorkerThread
public static @NonNull Recipient externalPush(@NonNull Context context, @NonNull SignalServiceAddress signalServiceAddress) {
return externalPush(context, signalServiceAddress.getUuid().orNull(), signalServiceAddress.getNumber().orNull());
return externalPush(context, signalServiceAddress.getUuid().orNull(), signalServiceAddress.getNumber().orNull(), false);
}
/**
* Returns a fully-populated {@link Recipient} based off of a {@link SignalServiceAddress},
* creating one in the database if necessary. This should only used for high-trust sources,
* which are limited to:
* - Envelopes
* - UD Certs
* - CDS
* - Storage Service
*/
@WorkerThread
public static @NonNull Recipient externalHighTrustPush(@NonNull Context context, @NonNull SignalServiceAddress signalServiceAddress) {
return externalPush(context, signalServiceAddress.getUuid().orNull(), signalServiceAddress.getNumber().orNull(), true);
}
/**
@ -152,73 +167,20 @@ public class Recipient {
* In particular, while we'll eventually get the UUID of a user created via a phone number
* (through a directory sync), the only way we can store the phone number is by retrieving it from
* sent messages and whatnot. So we should store it when available.
*
* @param highTrust This should only be set to true if the source of the E164-UUID pairing is one
* that can be trusted as accurate (like an envelope).
*/
@WorkerThread
public static @NonNull Recipient externalPush(@NonNull Context context, @Nullable UUID uuid, @Nullable String e164) {
public static @NonNull Recipient externalPush(@NonNull Context context, @Nullable UUID uuid, @Nullable String e164, boolean highTrust) {
if (UuidUtil.UNKNOWN_UUID.equals(uuid)) {
throw new AssertionError();
}
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(context);
Optional<RecipientId> uuidUser = uuid != null ? db.getByUuid(uuid) : Optional.absent();
Optional<RecipientId> e164User = e164 != null ? db.getByE164(e164) : Optional.absent();
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(context);
RecipientId recipientId = db.getAndPossiblyMerge(uuid, e164, highTrust);
if (uuidUser.isPresent()) {
Recipient recipient = resolved(uuidUser.get());
if (e164 != null && !recipient.getE164().isPresent() && !e164User.isPresent()) {
db.setPhoneNumber(recipient.getId(), e164);
}
return resolved(recipient.getId());
} else if (e164User.isPresent()) {
Recipient recipient = resolved(e164User.get());
if (uuid != null && !recipient.getUuid().isPresent()) {
db.markRegistered(recipient.getId(), uuid);
} else if (!recipient.isRegistered()) {
db.markRegistered(recipient.getId());
if (FeatureFlags.cds()) {
Log.i(TAG, "No UUID! Scheduling a fetch.");
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(recipient, false));
}
}
return resolved(recipient.getId());
} else if (uuid != null) {
if (FeatureFlags.uuidOnlyContacts() || e164 != null) {
RecipientId id = db.getOrInsertFromUuid(uuid);
db.markRegistered(id, uuid);
if (e164 != null) {
db.setPhoneNumber(id, e164);
}
return resolved(id);
} else {
if (!FeatureFlags.uuidOnlyContacts() && FeatureFlags.groupsV2()) {
throw new RuntimeException(new UuidRecipientError());
} else {
throw new UuidRecipientError();
}
}
} else if (e164 != null) {
Recipient recipient = resolved(db.getOrInsertFromE164(e164));
if (!recipient.isRegistered()) {
db.markRegistered(recipient.getId());
if (FeatureFlags.cds()) {
Log.i(TAG, "No UUID! Scheduling a fetch.");
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(recipient, false));
}
}
return resolved(recipient.getId());
} else {
throw new AssertionError("You must provide either a UUID or phone number!");
}
return resolved(recipientId);
}
/**
@ -259,7 +221,7 @@ public class Recipient {
* or serialized groupId.
*
* If the identifier is a UUID of a Signal user, prefer using
* {@link #externalPush(Context, UUID, String)} or its overload, as this will let us associate
* {@link #externalPush(Context, UUID, String, boolean)} or its overload, as this will let us associate
* the phone number with the recipient.
*/
@WorkerThread

View File

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.recipients;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
@ -49,7 +50,16 @@ public class RecipientId implements Parcelable, Comparable<RecipientId> {
@AnyThread
public static @NonNull RecipientId from(@NonNull SignalServiceAddress address) {
return from(address.getUuid().orNull(), address.getNumber().orNull());
return from(address.getUuid().orNull(), address.getNumber().orNull(), false);
}
/**
* Indicates that the pairing is from a high-trust source.
* See {@link Recipient#externalHighTrustPush(Context, SignalServiceAddress)}
*/
@AnyThread
public static @NonNull RecipientId fromHighTrust(@NonNull SignalServiceAddress address) {
return from(address.getUuid().orNull(), address.getNumber().orNull(), true);
}
/**
@ -58,15 +68,26 @@ public class RecipientId implements Parcelable, Comparable<RecipientId> {
@AnyThread
@SuppressLint("WrongThread")
public static @NonNull RecipientId from(@Nullable UUID uuid, @Nullable String e164) {
return from(uuid, e164, false);
}
@AnyThread
@SuppressLint("WrongThread")
private static @NonNull RecipientId from(@Nullable UUID uuid, @Nullable String e164, boolean highTrust) {
RecipientId recipientId = RecipientIdCache.INSTANCE.get(uuid, e164);
if (recipientId == null) {
recipientId = Recipient.externalPush(ApplicationDependencies.getApplication(), uuid, e164).getId();
recipientId = Recipient.externalPush(ApplicationDependencies.getApplication(), uuid, e164, highTrust).getId();
}
return recipientId;
}
@AnyThread
public static void clearCache() {
RecipientIdCache.INSTANCE.clear();
}
private RecipientId(long id) {
this.id = id;
}

View File

@ -70,4 +70,8 @@ final class RecipientIdCache {
return null;
}
synchronized void clear() {
ids.clear();
}
}

View File

@ -240,7 +240,7 @@ public final class CodeVerificationRequest {
RecipientId selfId = recipientDatabase.getOrInsertFromE164(credentials.getE164number());
recipientDatabase.setProfileSharing(selfId, true);
recipientDatabase.markRegistered(selfId, uuid);
recipientDatabase.markRegisteredOrThrow(selfId, uuid);
TextSecurePreferences.setLocalNumber(context, credentials.getE164number());
TextSecurePreferences.setLocalUuid(context, uuid);

View File

@ -101,15 +101,8 @@ public class MessageSender {
Recipient recipient = message.getRecipient();
boolean keyExchange = message.isKeyExchange();
long allocatedThreadId;
if (threadId == -1) {
allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
} else {
allocatedThreadId = threadId;
}
long messageId = database.insertMessageOutbox(allocatedThreadId, message, forceSms, System.currentTimeMillis(), insertListener);
long allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateValidThreadId(recipient, threadId);
long messageId = database.insertMessageOutbox(allocatedThreadId, message, forceSms, System.currentTimeMillis(), insertListener);
sendTextMessage(context, recipient, forceSms, keyExchange, messageId);
onMessageSent();
@ -127,16 +120,9 @@ public class MessageSender {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
long allocatedThreadId;
if (threadId == -1) {
allocatedThreadId = threadDatabase.getThreadIdFor(message.getRecipient(), message.getDistributionType());
} else {
allocatedThreadId = threadId;
}
Recipient recipient = message.getRecipient();
long messageId = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
long allocatedThreadId = threadDatabase.getOrCreateValidThreadId(message.getRecipient(), threadId, message.getDistributionType());
Recipient recipient = message.getRecipient();
long messageId = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
sendMediaMessage(context, recipient, forceSms, messageId, Collections.emptyList());
onMessageSent();

View File

@ -7,6 +7,9 @@ import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -39,6 +42,22 @@ public final class SqlUtil {
return false;
}
public static String[] buildArgs(Object... objects) {
String[] args = new String[objects.length];
for (int i = 0; i < objects.length; i++) {
if (objects[i] == null) {
throw new NullPointerException("Cannot have null arg!");
} else if (objects[i] instanceof RecipientId) {
args[i] = ((RecipientId) objects[i]).serialize();
} else {
args[i] = objects[i].toString();
}
}
return args;
}
/**
* Returns an updated query and args pairing that will only update rows that would *actually*
* change. In other words, if {@link SQLiteDatabase#update(String, ContentValues, String, String[])}

View File

@ -7,6 +7,7 @@
package org.whispersystems.signalservice.api.push;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.UUID;
@ -43,8 +44,7 @@ public class SignalServiceAddress {
* Convenience constructor that will consider a UUID/E164 string absent if it is null or empty.
*/
public SignalServiceAddress(UUID uuid, String e164) {
this(Optional.fromNullable(uuid),
e164 != null && !e164.isEmpty() ? Optional.of(e164) : Optional.<String>absent());
this(Optional.fromNullable(uuid), OptionalUtil.absentIfEmpty(e164));
}
public SignalServiceAddress(Optional<UUID> uuid, Optional<String> e164) {