kopia lustrzana https://github.com/ryukoposting/Signal-Android
283 wiersze
16 KiB
Java
283 wiersze
16 KiB
Java
package org.thoughtcrime.securesms.database.helpers;
|
|
|
|
import android.content.ContentValues;
|
|
import android.database.Cursor;
|
|
import android.text.TextUtils;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
|
|
|
import org.signal.core.util.logging.Log;
|
|
import org.thoughtcrime.securesms.groups.GroupId;
|
|
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
|
import org.thoughtcrime.securesms.util.DelimiterUtil;
|
|
import org.thoughtcrime.securesms.util.Util;
|
|
|
|
import java.util.HashSet;
|
|
import java.util.Set;
|
|
|
|
public class RecipientIdMigrationHelper {
|
|
|
|
private static final String TAG = Log.tag(RecipientIdMigrationHelper.class);
|
|
|
|
public static void execute(SQLiteDatabase db) {
|
|
Log.i(TAG, "Starting the recipient ID migration.");
|
|
|
|
long insertStart = System.currentTimeMillis();
|
|
|
|
Log.i(TAG, "Starting inserts for missing recipients.");
|
|
db.execSQL(buildInsertMissingRecipientStatement("identities", "address"));
|
|
db.execSQL(buildInsertMissingRecipientStatement("sessions", "address"));
|
|
db.execSQL(buildInsertMissingRecipientStatement("thread", "recipient_ids"));
|
|
db.execSQL(buildInsertMissingRecipientStatement("sms", "address"));
|
|
db.execSQL(buildInsertMissingRecipientStatement("mms", "address"));
|
|
db.execSQL(buildInsertMissingRecipientStatement("mms", "quote_author"));
|
|
db.execSQL(buildInsertMissingRecipientStatement("group_receipts", "address"));
|
|
db.execSQL(buildInsertMissingRecipientStatement("groups", "group_id"));
|
|
Log.i(TAG, "Finished inserts for missing recipients in " + (System.currentTimeMillis() - insertStart) + " ms.");
|
|
|
|
long updateMissingStart = System.currentTimeMillis();
|
|
|
|
Log.i(TAG, "Starting updates for invalid or missing addresses.");
|
|
db.execSQL(buildMissingAddressUpdateStatement("sms", "address"));
|
|
db.execSQL(buildMissingAddressUpdateStatement("mms", "address"));
|
|
db.execSQL(buildMissingAddressUpdateStatement("mms", "quote_author"));
|
|
Log.i(TAG, "Finished updates for invalid or missing addresses in " + (System.currentTimeMillis() - updateMissingStart) + " ms.");
|
|
|
|
db.execSQL("ALTER TABLE groups ADD COLUMN recipient_id INTEGER DEFAULT 0");
|
|
|
|
long updateStart = System.currentTimeMillis();
|
|
|
|
Log.i(TAG, "Starting recipient ID updates.");
|
|
db.execSQL(buildUpdateAddressToRecipientIdStatement("identities", "address"));
|
|
db.execSQL(buildUpdateAddressToRecipientIdStatement("sessions", "address"));
|
|
db.execSQL(buildUpdateAddressToRecipientIdStatement("thread", "recipient_ids"));
|
|
db.execSQL(buildUpdateAddressToRecipientIdStatement("sms", "address"));
|
|
db.execSQL(buildUpdateAddressToRecipientIdStatement("mms", "address"));
|
|
db.execSQL(buildUpdateAddressToRecipientIdStatement("mms", "quote_author"));
|
|
db.execSQL(buildUpdateAddressToRecipientIdStatement("group_receipts", "address"));
|
|
db.execSQL("UPDATE groups SET recipient_id = (SELECT _id FROM recipient_preferences WHERE recipient_preferences.recipient_ids = groups.group_id)");
|
|
Log.i(TAG, "Finished recipient ID updates in " + (System.currentTimeMillis() - updateStart) + " ms.");
|
|
|
|
// NOTE: Because there's an open cursor on the same table, inserts and updates aren't visible
|
|
// until afterwards, which is why this group stuff is split into multiple loops
|
|
|
|
long findGroupStart = System.currentTimeMillis();
|
|
|
|
Log.i(TAG, "Starting to find missing group recipients.");
|
|
Set<String> missingGroupMembers = new HashSet<>();
|
|
|
|
try (Cursor cursor = db.rawQuery("SELECT members FROM groups", null)) {
|
|
while (cursor != null && cursor.moveToNext()) {
|
|
String serializedMembers = cursor.getString(cursor.getColumnIndexOrThrow("members"));
|
|
String[] members = DelimiterUtil.split(serializedMembers, ',');
|
|
|
|
for (String rawMember : members) {
|
|
String member = DelimiterUtil.unescape(rawMember, ',');
|
|
|
|
if (!TextUtils.isEmpty(member) && !recipientExists(db, member)) {
|
|
missingGroupMembers.add(member);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Log.i(TAG, "Finished finding " + missingGroupMembers.size() + " missing group recipients in " + (System.currentTimeMillis() - findGroupStart) + " ms.");
|
|
|
|
long insertGroupStart = System.currentTimeMillis();
|
|
|
|
Log.i(TAG, "Starting the insert of missing group recipients.");
|
|
for (String member : missingGroupMembers) {
|
|
ContentValues values = new ContentValues();
|
|
values.put("recipient_ids", member);
|
|
db.insert("recipient_preferences", null, values);
|
|
}
|
|
Log.i(TAG, "Finished inserting missing group recipients in " + (System.currentTimeMillis() - insertGroupStart) + " ms.");
|
|
|
|
long updateGroupStart = System.currentTimeMillis();
|
|
|
|
Log.i(TAG, "Starting group recipient ID updates.");
|
|
try (Cursor cursor = db.rawQuery("SELECT _id, members FROM groups", null)) {
|
|
while (cursor != null && cursor.moveToNext()) {
|
|
long groupId = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
|
|
String serializedMembers = cursor.getString(cursor.getColumnIndexOrThrow("members"));
|
|
String[] members = DelimiterUtil.split(serializedMembers, ',');
|
|
long[] memberIds = new long[members.length];
|
|
|
|
for (int i = 0; i < members.length; i++) {
|
|
String member = DelimiterUtil.unescape(members[i], ',');
|
|
memberIds[i] = requireRecipientId(db, member);
|
|
}
|
|
|
|
String serializedMemberIds = Util.join(memberIds, ",");
|
|
|
|
db.execSQL("UPDATE groups SET members = ? WHERE _id = ?", new String[]{ serializedMemberIds, String.valueOf(groupId) });
|
|
}
|
|
}
|
|
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS group_recipient_id_index ON groups (recipient_id)");
|
|
Log.i(TAG, "Finished group recipient ID updates in " + (System.currentTimeMillis() - updateGroupStart) + " ms.");
|
|
|
|
|
|
long tableCopyStart = System.currentTimeMillis();
|
|
|
|
Log.i(TAG, "Starting to copy the recipient table.");
|
|
db.execSQL("CREATE TABLE recipient (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
"uuid TEXT UNIQUE DEFAULT NULL, " +
|
|
"phone TEXT UNIQUE DEFAULT NULL, " +
|
|
"email TEXT UNIQUE DEFAULT NULL, " +
|
|
"group_id TEXT UNIQUE DEFAULT NULL, " +
|
|
"blocked INTEGER DEFAULT 0, " +
|
|
"message_ringtone TEXT DEFAULT NULL, " +
|
|
"message_vibrate INTEGER DEFAULT 0, " +
|
|
"call_ringtone TEXT DEFAULT NULL, " +
|
|
"call_vibrate INTEGER DEFAULT 0, " +
|
|
"notification_channel TEXT DEFAULT NULL, " +
|
|
"mute_until INTEGER DEFAULT 0, " +
|
|
"color TEXT DEFAULT NULL, " +
|
|
"seen_invite_reminder INTEGER DEFAULT 0, " +
|
|
"default_subscription_id INTEGER DEFAULT -1, " +
|
|
"message_expiration_time INTEGER DEFAULT 0, " +
|
|
"registered INTEGER DEFAULT 0, " +
|
|
"system_display_name TEXT DEFAULT NULL, " +
|
|
"system_photo_uri TEXT DEFAULT NULL, " +
|
|
"system_phone_label TEXT DEFAULT NULL, " +
|
|
"system_contact_uri TEXT DEFAULT NULL, " +
|
|
"profile_key TEXT DEFAULT NULL, " +
|
|
"signal_profile_name TEXT DEFAULT NULL, " +
|
|
"signal_profile_avatar TEXT DEFAULT NULL, " +
|
|
"profile_sharing INTEGER DEFAULT 0, " +
|
|
"unidentified_access_mode INTEGER DEFAULT 0, " +
|
|
"force_sms_selection INTEGER DEFAULT 0)");
|
|
|
|
try (Cursor cursor = db.query("recipient_preferences", null, null, null, null, null, null)) {
|
|
while (cursor != null && cursor.moveToNext()) {
|
|
String address = cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids"));
|
|
boolean isGroup = GroupId.isEncodedGroup(address);
|
|
boolean isEmail = !isGroup && NumberUtil.isValidEmail(address);
|
|
boolean isPhone = !isGroup && !isEmail;
|
|
|
|
ContentValues values = new ContentValues();
|
|
|
|
values.put("_id", cursor.getLong(cursor.getColumnIndexOrThrow("_id")));
|
|
values.put("uuid", (String) null);
|
|
values.put("phone", isPhone ? address : null);
|
|
values.put("email", isEmail ? address : null);
|
|
values.put("group_id", isGroup ? address : null);
|
|
values.put("blocked", cursor.getInt(cursor.getColumnIndexOrThrow("block")));
|
|
values.put("message_ringtone", cursor.getString(cursor.getColumnIndexOrThrow("notification")));
|
|
values.put("message_vibrate", cursor.getString(cursor.getColumnIndexOrThrow("vibrate")));
|
|
values.put("call_ringtone", cursor.getString(cursor.getColumnIndexOrThrow("call_ringtone")));
|
|
values.put("call_vibrate", cursor.getString(cursor.getColumnIndexOrThrow("call_vibrate")));
|
|
values.put("notification_channel", cursor.getString(cursor.getColumnIndexOrThrow("notification_channel")));
|
|
values.put("mute_until", cursor.getLong(cursor.getColumnIndexOrThrow("mute_until")));
|
|
values.put("color", cursor.getString(cursor.getColumnIndexOrThrow("color")));
|
|
values.put("seen_invite_reminder", cursor.getInt(cursor.getColumnIndexOrThrow("seen_invite_reminder")));
|
|
values.put("default_subscription_id", cursor.getInt(cursor.getColumnIndexOrThrow("default_subscription_id")));
|
|
values.put("message_expiration_time", cursor.getInt(cursor.getColumnIndexOrThrow("expire_messages")));
|
|
values.put("registered", cursor.getInt(cursor.getColumnIndexOrThrow("registered")));
|
|
values.put("system_display_name", cursor.getString(cursor.getColumnIndexOrThrow("system_display_name")));
|
|
values.put("system_photo_uri", cursor.getString(cursor.getColumnIndexOrThrow("system_contact_photo")));
|
|
values.put("system_phone_label", cursor.getString(cursor.getColumnIndexOrThrow("system_phone_label")));
|
|
values.put("system_contact_uri", cursor.getString(cursor.getColumnIndexOrThrow("system_contact_uri")));
|
|
values.put("profile_key", cursor.getString(cursor.getColumnIndexOrThrow("profile_key")));
|
|
values.put("signal_profile_name", cursor.getString(cursor.getColumnIndexOrThrow("signal_profile_name")));
|
|
values.put("signal_profile_avatar", cursor.getString(cursor.getColumnIndexOrThrow("signal_profile_avatar")));
|
|
values.put("profile_sharing", cursor.getInt(cursor.getColumnIndexOrThrow("profile_sharing_approval")));
|
|
values.put("unidentified_access_mode", cursor.getInt(cursor.getColumnIndexOrThrow("unidentified_access_mode")));
|
|
values.put("force_sms_selection", cursor.getInt(cursor.getColumnIndexOrThrow("force_sms_selection")));
|
|
|
|
db.insert("recipient", null, values);
|
|
}
|
|
}
|
|
|
|
db.execSQL("DROP TABLE recipient_preferences");
|
|
Log.i(TAG, "Finished copying the recipient table in " + (System.currentTimeMillis() - tableCopyStart) + " ms.");
|
|
|
|
long sanityCheckStart = System.currentTimeMillis();
|
|
|
|
Log.i(TAG, "Starting DB integrity sanity checks.");
|
|
assertEmptyQuery(db, "identities", buildSanityCheckQuery("identities", "address"));
|
|
assertEmptyQuery(db, "sessions", buildSanityCheckQuery("sessions", "address"));
|
|
assertEmptyQuery(db, "groups", buildSanityCheckQuery("groups", "recipient_id"));
|
|
assertEmptyQuery(db, "thread", buildSanityCheckQuery("thread", "recipient_ids"));
|
|
assertEmptyQuery(db, "sms", buildSanityCheckQuery("sms", "address"));
|
|
assertEmptyQuery(db, "mms -- address", buildSanityCheckQuery("mms", "address"));
|
|
assertEmptyQuery(db, "mms -- quote_author", buildSanityCheckQuery("mms", "quote_author"));
|
|
assertEmptyQuery(db, "group_receipts", buildSanityCheckQuery("group_receipts", "address"));
|
|
Log.i(TAG, "Finished DB integrity sanity checks in " + (System.currentTimeMillis() - sanityCheckStart) + " ms.");
|
|
|
|
Log.i(TAG, "Finished recipient ID migration in " + (System.currentTimeMillis() - insertStart) + " ms.");
|
|
}
|
|
|
|
private static String buildUpdateAddressToRecipientIdStatement(@NonNull String table, @NonNull String addressColumn) {
|
|
return "UPDATE " + table + " SET " + addressColumn + "=(SELECT _id " +
|
|
"FROM recipient_preferences " +
|
|
"WHERE recipient_preferences.recipient_ids = " + table + "." + addressColumn + ")";
|
|
}
|
|
|
|
private static String buildInsertMissingRecipientStatement(@NonNull String table, @NonNull String addressColumn) {
|
|
return "INSERT INTO recipient_preferences(recipient_ids) SELECT DISTINCT " + addressColumn + " " +
|
|
"FROM " + table + " " +
|
|
"WHERE " + addressColumn + " != '' AND " +
|
|
addressColumn + " != 'insert-address-column' AND " +
|
|
addressColumn + " NOT NULL AND " +
|
|
addressColumn + " NOT IN (SELECT recipient_ids FROM recipient_preferences)";
|
|
}
|
|
|
|
private static String buildMissingAddressUpdateStatement(@NonNull String table, @NonNull String addressColumn) {
|
|
return "UPDATE " + table + " SET " + addressColumn + " = -1 " +
|
|
"WHERE " + addressColumn + " = '' OR " +
|
|
addressColumn + " IS NULL OR " +
|
|
addressColumn + " = 'insert-address-token'";
|
|
}
|
|
|
|
private static boolean recipientExists(@NonNull SQLiteDatabase db, @NonNull String address) {
|
|
return getRecipientId(db, address) != null;
|
|
}
|
|
|
|
private static @Nullable Long getRecipientId(@NonNull SQLiteDatabase db, @NonNull String address) {
|
|
try (Cursor cursor = db.rawQuery("SELECT _id FROM recipient_preferences WHERE recipient_ids = ?", new String[]{ address })) {
|
|
if (cursor != null && cursor.moveToFirst()) {
|
|
return cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static long requireRecipientId(@NonNull SQLiteDatabase db, @NonNull String address) {
|
|
Long id = getRecipientId(db, address);
|
|
|
|
if (id != null) {
|
|
return id;
|
|
} else {
|
|
throw new MissingRecipientError(address);
|
|
}
|
|
}
|
|
|
|
private static String buildSanityCheckQuery(@NonNull String table, @NonNull String idColumn) {
|
|
return "SELECT " + idColumn + " FROM " + table + " WHERE " + idColumn + " != -1 AND " + idColumn + " NOT IN (SELECT _id FROM recipient)";
|
|
}
|
|
|
|
private static void assertEmptyQuery(@NonNull SQLiteDatabase db, @NonNull String tag, @NonNull String query) {
|
|
try (Cursor cursor = db.rawQuery(query, null)) {
|
|
if (cursor != null && cursor.moveToFirst()) {
|
|
throw new FailedSanityCheckError(tag);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static final class MissingRecipientError extends AssertionError {
|
|
MissingRecipientError(@NonNull String address) {
|
|
super("Could not find recipient with address " + address);
|
|
}
|
|
}
|
|
|
|
private static final class FailedSanityCheckError extends AssertionError {
|
|
FailedSanityCheckError(@NonNull String tableName) {
|
|
super("Sanity check failed for tag '" + tableName + "'");
|
|
}
|
|
}
|
|
}
|