mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 18:00:02 +01:00
Handle UUID-only recipients and merging.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user