mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 21:38:33 +00:00
Handle UUID-only recipients and merging.
This commit is contained in:
parent
644af87782
commit
bd078fc883
@ -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,25 +103,7 @@ 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);
|
||||
}
|
||||
|
||||
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)
|
||||
@ -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());
|
||||
}
|
||||
|
@ -62,6 +62,7 @@ public class DatabaseFactory {
|
||||
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) {
|
||||
@ -208,6 +213,7 @@ public class DatabaseFactory {
|
||||
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;
|
||||
}
|
||||
@ -422,11 +575,17 @@ public class RecipientDatabase extends Database {
|
||||
try (Cursor cursor = database.query(table, RECIPIENT_FULL_PROJECTION, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
return getRecipientSettings(context, cursor);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull DirtyState getDirtyState(@NonNull RecipientId recipientId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
@ -527,24 +686,40 @@ 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()) {
|
||||
try {
|
||||
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.");
|
||||
} else {
|
||||
int count = db.update(TABLE_NAME, values, UUID + " = ?", new String[] { insert.getAddress().getUuid().get().toString() });
|
||||
Log.w(TAG, "Updated " + count + " users by UUID.");
|
||||
} 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 {
|
||||
RecipientId recipientId = RecipientId.from(id);
|
||||
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);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
recipientId = RecipientId.from(id);
|
||||
}
|
||||
|
||||
if (insert.getIdentityKey().isPresent()) {
|
||||
try {
|
||||
@ -559,15 +734,26 @@ public class RecipientDatabase extends Database {
|
||||
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())});
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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,9 +1633,17 @@ public class RecipientDatabase extends Database {
|
||||
values.put(UUID, entry.getValue().toLowerCase());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
for (RecipientId id : unregistered) {
|
||||
@ -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,8 +2029,14 @@ public class RecipientDatabase extends Database {
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
|
||||
for (RecipientId id : recipients) {
|
||||
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();
|
||||
} finally {
|
||||
@ -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) {
|
||||
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();
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -70,4 +70,8 @@ final class RecipientIdCache {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
synchronized void clear() {
|
||||
ids.clear();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -101,14 +101,7 @@ 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 allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateValidThreadId(recipient, threadId);
|
||||
long messageId = database.insertMessageOutbox(allocatedThreadId, message, forceSms, System.currentTimeMillis(), insertListener);
|
||||
|
||||
sendTextMessage(context, recipient, forceSms, keyExchange, messageId);
|
||||
@ -127,14 +120,7 @@ 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;
|
||||
}
|
||||
|
||||
long allocatedThreadId = threadDatabase.getOrCreateValidThreadId(message.getRecipient(), threadId, message.getDistributionType());
|
||||
Recipient recipient = message.getRecipient();
|
||||
long messageId = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
|
||||
|
||||
|
@ -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[])}
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user