diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsRepository.kt index 01ad530bee..a955ca8e9c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsRepository.kt @@ -18,7 +18,7 @@ class PrivacySettingsRepository { SignalExecutors.BOUNDED.execute { val recipientDatabase = SignalDatabase.recipients - consumer(recipientDatabase.blocked.count) + consumer(recipientDatabase.getBlocked().count) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java index 5ae92eabf5..2b8a80bd03 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java @@ -134,7 +134,7 @@ public enum AvatarColor { return name; } - public static @NonNull AvatarColor deserialize(@NonNull String name) { + public static @NonNull AvatarColor deserialize(@Nullable String name) { return Objects.requireNonNull(NAME_MAP.getOrDefault(name, A210)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/CursorExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/database/CursorExtensions.kt new file mode 100644 index 0000000000..6f1eebb027 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/CursorExtensions.kt @@ -0,0 +1,57 @@ +package org.thoughtcrime.securesms.database + +import android.database.Cursor +import org.thoughtcrime.securesms.util.CursorUtil +import org.whispersystems.libsignal.util.guava.Optional + +fun Cursor.requireString(column: String): String? { + return CursorUtil.requireString(this, column) +} + +fun Cursor.requireNonNullString(column: String): String { + return CursorUtil.requireString(this, column)!! +} + +fun Cursor.optionalString(column: String): Optional { + return CursorUtil.getString(this, column) +} + +fun Cursor.requireInt(column: String): Int { + return CursorUtil.requireInt(this, column) +} + +fun Cursor.optionalInt(column: String): Optional { + return CursorUtil.getInt(this, column) +} + +fun Cursor.requireFloat(column: String): Float { + return CursorUtil.requireFloat(this, column) +} + +fun Cursor.requireLong(column: String): Long { + return CursorUtil.requireLong(this, column) +} + +fun Cursor.requireBoolean(column: String): Boolean { + return CursorUtil.requireInt(this, column) != 0 +} + +fun Cursor.optionalBoolean(column: String): Optional { + return CursorUtil.getBoolean(this, column) +} + +fun Cursor.requireBlob(column: String): ByteArray? { + return CursorUtil.requireBlob(this, column) +} + +fun Cursor.requireNonNullBlob(column: String): ByteArray { + return CursorUtil.requireBlob(this, column)!! +} + +fun Cursor.optionalBlob(column: String): Optional { + return CursorUtil.getBlob(this, column) +} + +fun Cursor.isNull(column: String): Boolean { + return CursorUtil.isNull(this, column) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Database.java b/app/src/main/java/org/thoughtcrime/securesms/database/Database.java index 7d0ca7c92d..38924d8a94 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Database.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Database.java @@ -74,4 +74,12 @@ public abstract class Database { public void reset(SignalDatabase databaseHelper) { this.databaseHelper = databaseHelper; } + + protected SQLiteDatabase getReadableDatabase() { + return databaseHelper.getSignalReadableDatabase(); + } + + protected SQLiteDatabase getWritableDatabase() { + return databaseHelper.getSignalWritableDatabase(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java deleted file mode 100644 index 1ae10feb1b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ /dev/null @@ -1,3897 +0,0 @@ -package org.thoughtcrime.securesms.database; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import com.annimon.stream.Stream; -import com.google.protobuf.ByteString; -import com.google.protobuf.InvalidProtocolBufferException; - -import net.zetetic.database.sqlcipher.SQLiteConstraintException; - -import org.jetbrains.annotations.NotNull; -import org.signal.core.util.logging.Log; -import org.signal.storageservice.protos.groups.local.DecryptedGroup; -import org.signal.zkgroup.InvalidInputException; -import org.signal.zkgroup.groups.GroupMasterKey; -import org.signal.zkgroup.profiles.ProfileKey; -import org.signal.zkgroup.profiles.ProfileKeyCredential; -import org.thoughtcrime.securesms.badges.Badges; -import org.thoughtcrime.securesms.badges.models.Badge; -import org.thoughtcrime.securesms.color.MaterialColor; -import org.thoughtcrime.securesms.conversation.colors.AvatarColor; -import org.thoughtcrime.securesms.conversation.colors.ChatColors; -import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper; -import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; -import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore; -import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.whispersystems.signalservice.api.push.ACI; -import org.thoughtcrime.securesms.database.model.IdentityRecord; -import org.thoughtcrime.securesms.database.model.ThreadRecord; -import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList; -import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor; -import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime; -import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileKeyCredentialColumnData; -import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras; -import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.groups.BadGroupIdException; -import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.groups.v2.ProfileKeySet; -import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor; -import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob; -import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; -import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob; -import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; -import org.thoughtcrime.securesms.profiles.AvatarHelper; -import org.thoughtcrime.securesms.profiles.ProfileName; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.storage.StorageRecordUpdate; -import org.thoughtcrime.securesms.storage.StorageSyncHelper; -import org.thoughtcrime.securesms.storage.StorageSyncModels; -import org.thoughtcrime.securesms.util.Base64; -import org.thoughtcrime.securesms.util.Bitmask; -import org.thoughtcrime.securesms.util.CursorUtil; -import org.thoughtcrime.securesms.util.GroupUtil; -import org.thoughtcrime.securesms.util.IdentityUtil; -import org.thoughtcrime.securesms.util.SqlUtil; -import org.thoughtcrime.securesms.util.StringUtil; -import org.thoughtcrime.securesms.util.Util; -import org.thoughtcrime.securesms.wallpaper.ChatWallpaper; -import org.thoughtcrime.securesms.wallpaper.ChatWallpaperFactory; -import org.thoughtcrime.securesms.wallpaper.WallpaperStorage; -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; -import org.whispersystems.signalservice.api.storage.SignalAccountRecord; -import org.whispersystems.signalservice.api.storage.SignalContactRecord; -import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; -import org.whispersystems.signalservice.api.storage.SignalGroupV2Record; -import org.whispersystems.signalservice.api.storage.StorageId; - -import java.io.Closeable; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -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.concurrent.TimeUnit; -import java.util.function.Function; - -public class RecipientDatabase extends Database { - - private static final String TAG = Log.tag(RecipientDatabase.class); - - static final String TABLE_NAME = "recipient"; - public static final String ID = "_id"; - private static final String ACI_COLUMN = "uuid"; - private static final String USERNAME = "username"; - public static final String PHONE = "phone"; - public static final String EMAIL = "email"; - static final String GROUP_ID = "group_id"; - static final String GROUP_TYPE = "group_type"; - private static final String BLOCKED = "blocked"; - private static final String MESSAGE_RINGTONE = "message_ringtone"; - private static final String MESSAGE_VIBRATE = "message_vibrate"; - private static final String CALL_RINGTONE = "call_ringtone"; - private static final String CALL_VIBRATE = "call_vibrate"; - private static final String NOTIFICATION_CHANNEL = "notification_channel"; - private static final String MUTE_UNTIL = "mute_until"; - private static final String AVATAR_COLOR = "color"; - private static final String SEEN_INVITE_REMINDER = "seen_invite_reminder"; - private static final String DEFAULT_SUBSCRIPTION_ID = "default_subscription_id"; - private static final String MESSAGE_EXPIRATION_TIME = "message_expiration_time"; - public static final String REGISTERED = "registered"; - public static final String SYSTEM_JOINED_NAME = "system_display_name"; - public static final String SYSTEM_FAMILY_NAME = "system_family_name"; - public static final String SYSTEM_GIVEN_NAME = "system_given_name"; - private static final String SYSTEM_PHOTO_URI = "system_photo_uri"; - public static final String SYSTEM_PHONE_TYPE = "system_phone_type"; - public static final String SYSTEM_PHONE_LABEL = "system_phone_label"; - private static final String SYSTEM_CONTACT_URI = "system_contact_uri"; - private static final String SYSTEM_INFO_PENDING = "system_info_pending"; - private static final String PROFILE_KEY = "profile_key"; - private static final String PROFILE_KEY_CREDENTIAL = "profile_key_credential"; - private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar"; - private static final String PROFILE_SHARING = "profile_sharing"; - private static final String LAST_PROFILE_FETCH = "last_profile_fetch"; - private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode"; - static final String FORCE_SMS_SELECTION = "force_sms_selection"; - private static final String CAPABILITIES = "capabilities"; - private static final String STORAGE_SERVICE_ID = "storage_service_key"; - private static final String PROFILE_GIVEN_NAME = "signal_profile_name"; - private static final String PROFILE_FAMILY_NAME = "profile_family_name"; - private static final String PROFILE_JOINED_NAME = "profile_joined_name"; - private static final String MENTION_SETTING = "mention_setting"; - private static final String STORAGE_PROTO = "storage_proto"; - private static final String LAST_SESSION_RESET = "last_session_reset"; - private static final String WALLPAPER = "wallpaper"; - private static final String WALLPAPER_URI = "wallpaper_file"; - public static final String ABOUT = "about"; - public static final String ABOUT_EMOJI = "about_emoji"; - private static final String EXTRAS = "extras"; - private static final String GROUPS_IN_COMMON = "groups_in_common"; - private static final String CHAT_COLORS = "chat_colors"; - private static final String CUSTOM_CHAT_COLORS_ID = "custom_chat_colors_id"; - private static final String BADGES = "badges"; - - public static final String SEARCH_PROFILE_NAME = "search_signal_profile"; - private static final String SORT_NAME = "sort_name"; - private static final String IDENTITY_STATUS = "identity_status"; - private static final String IDENTITY_KEY = "identity_key"; - - /** - * Values that represent the index in the capabilities bitmask. Each index can store a 2-bit - * value, which in this case is the value of {@link Recipient.Capability}. - */ - private static final class Capabilities { - static final int BIT_LENGTH = 2; - - static final int GROUPS_V2 = 0; - static final int GROUPS_V1_MIGRATION = 1; - static final int SENDER_KEY = 2; - static final int ANNOUNCEMENT_GROUPS = 3; - static final int CHANGE_NUMBER = 4; - } - - private static final String[] RECIPIENT_PROJECTION = new String[] { - ID, ACI_COLUMN, USERNAME, PHONE, EMAIL, GROUP_ID, GROUP_TYPE, - BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, AVATAR_COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED, - PROFILE_KEY, PROFILE_KEY_CREDENTIAL, - SYSTEM_JOINED_NAME, SYSTEM_GIVEN_NAME, SYSTEM_FAMILY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI, - PROFILE_GIVEN_NAME, PROFILE_FAMILY_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, LAST_PROFILE_FETCH, - NOTIFICATION_CHANNEL, - UNIDENTIFIED_ACCESS_MODE, - FORCE_SMS_SELECTION, - CAPABILITIES, - STORAGE_SERVICE_ID, - MENTION_SETTING, WALLPAPER, WALLPAPER_URI, - MENTION_SETTING, - ABOUT, ABOUT_EMOJI, - EXTRAS, GROUPS_IN_COMMON, - CHAT_COLORS, CUSTOM_CHAT_COLORS_ID, - BADGES - }; - - private static final String[] ID_PROJECTION = new String[]{ID}; - private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_JOINED_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, ABOUT, ABOUT_EMOJI, EXTRAS, GROUPS_IN_COMMON, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "LOWER(COALESCE(" + nullIfEmpty(SYSTEM_JOINED_NAME) + ", " + nullIfEmpty(SYSTEM_GIVEN_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ")) AS " + SORT_NAME}; - public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_JOINED_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, ABOUT, ABOUT_EMOJI, EXTRAS, GROUPS_IN_COMMON, SEARCH_PROFILE_NAME, SORT_NAME}; - private static final String[] TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION) - .map(columnName -> TABLE_NAME + "." + columnName) - .toList().toArray(new String[0]); - - static final String[] TYPED_RECIPIENT_PROJECTION_NO_ID = Arrays.copyOfRange(TYPED_RECIPIENT_PROJECTION, 1, TYPED_RECIPIENT_PROJECTION.length); - - private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_JOINED_NAME) + ", " + nullIfEmpty(SYSTEM_GIVEN_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME}; - - public static final String[] CREATE_INDEXS = new String[] { - "CREATE INDEX IF NOT EXISTS recipient_group_type_index ON " + TABLE_NAME + " (" + GROUP_TYPE + ");", - }; - - public enum VibrateState { - DEFAULT(0), ENABLED(1), DISABLED(2); - - private final int id; - - VibrateState(int id) { - this.id = id; - } - - public int getId() { - return id; - } - - public static VibrateState fromId(int id) { - return values()[id]; - } - - public static VibrateState fromBoolean(boolean enabled) { - return enabled ? ENABLED : DISABLED; - } - } - - public enum RegisteredState { - UNKNOWN(0), REGISTERED(1), NOT_REGISTERED(2); - - private final int id; - - RegisteredState(int id) { - this.id = id; - } - - public int getId() { - return id; - } - - public static RegisteredState fromId(int id) { - return values()[id]; - } - } - - public enum UnidentifiedAccessMode { - UNKNOWN(0), DISABLED(1), ENABLED(2), UNRESTRICTED(3); - - private final int mode; - - UnidentifiedAccessMode(int mode) { - this.mode = mode; - } - - public int getMode() { - return mode; - } - - public static UnidentifiedAccessMode fromMode(int mode) { - return values()[mode]; - } - } - - public enum InsightsBannerTier { - NO_TIER(0), TIER_ONE(1), TIER_TWO(2); - - private final int id; - - InsightsBannerTier(int id) { - this.id = id; - } - - public int getId() { - return id; - } - - public boolean seen(InsightsBannerTier tier) { - return tier.getId() <= id; - } - - public static InsightsBannerTier fromId(int id) { - return values()[id]; - } - } - - public enum GroupType { - NONE(0), MMS(1), SIGNAL_V1(2), SIGNAL_V2(3); - - private final int id; - - GroupType(int id) { - this.id = id; - } - - int getId() { - return id; - } - - public static GroupType fromId(int id) { - return values()[id]; - } - } - - public enum MentionSetting { - ALWAYS_NOTIFY(0), DO_NOT_NOTIFY(1); - - private final int id; - - MentionSetting(int id) { - this.id = id; - } - - int getId() { - return id; - } - - public static MentionSetting fromId(int id) { - return values()[id]; - } - } - - public static final String CREATE_TABLE = - "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + - ACI_COLUMN + " TEXT UNIQUE DEFAULT NULL, " + - USERNAME + " TEXT UNIQUE DEFAULT NULL, " + - PHONE + " TEXT UNIQUE DEFAULT NULL, " + - EMAIL + " TEXT UNIQUE DEFAULT NULL, " + - GROUP_ID + " TEXT UNIQUE DEFAULT NULL, " + - GROUP_TYPE + " INTEGER DEFAULT " + GroupType.NONE.getId() + ", " + - BLOCKED + " INTEGER DEFAULT 0," + - MESSAGE_RINGTONE + " TEXT DEFAULT NULL, " + - MESSAGE_VIBRATE + " INTEGER DEFAULT " + VibrateState.DEFAULT.getId() + ", " + - CALL_RINGTONE + " TEXT DEFAULT NULL, " + - CALL_VIBRATE + " INTEGER DEFAULT " + VibrateState.DEFAULT.getId() + ", " + - NOTIFICATION_CHANNEL + " TEXT DEFAULT NULL, " + - MUTE_UNTIL + " INTEGER DEFAULT 0, " + - AVATAR_COLOR + " TEXT DEFAULT NULL, " + - SEEN_INVITE_REMINDER + " INTEGER DEFAULT " + InsightsBannerTier.NO_TIER.getId() + ", " + - DEFAULT_SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + - MESSAGE_EXPIRATION_TIME + " INTEGER DEFAULT 0, " + - REGISTERED + " INTEGER DEFAULT " + RegisteredState.UNKNOWN.getId() + ", " + - SYSTEM_GIVEN_NAME + " TEXT DEFAULT NULL, " + - SYSTEM_FAMILY_NAME + " TEXT DEFAULT NULL, " + - SYSTEM_JOINED_NAME + " TEXT DEFAULT NULL, " + - SYSTEM_PHOTO_URI + " TEXT DEFAULT NULL, " + - SYSTEM_PHONE_LABEL + " TEXT DEFAULT NULL, " + - SYSTEM_PHONE_TYPE + " INTEGER DEFAULT -1, " + - SYSTEM_CONTACT_URI + " TEXT DEFAULT NULL, " + - SYSTEM_INFO_PENDING + " INTEGER DEFAULT 0, " + - PROFILE_KEY + " TEXT DEFAULT NULL, " + - PROFILE_KEY_CREDENTIAL + " TEXT DEFAULT NULL, " + - PROFILE_GIVEN_NAME + " TEXT DEFAULT NULL, " + - PROFILE_FAMILY_NAME + " TEXT DEFAULT NULL, " + - PROFILE_JOINED_NAME + " TEXT DEFAULT NULL, " + - SIGNAL_PROFILE_AVATAR + " TEXT DEFAULT NULL, " + - PROFILE_SHARING + " INTEGER DEFAULT 0, " + - LAST_PROFILE_FETCH + " INTEGER DEFAULT 0, " + - UNIDENTIFIED_ACCESS_MODE + " INTEGER DEFAULT 0, " + - FORCE_SMS_SELECTION + " INTEGER DEFAULT 0, " + - STORAGE_SERVICE_ID + " TEXT UNIQUE DEFAULT NULL, " + - MENTION_SETTING + " INTEGER DEFAULT " + MentionSetting.ALWAYS_NOTIFY.getId() + ", " + - STORAGE_PROTO + " TEXT DEFAULT NULL, " + - CAPABILITIES + " INTEGER DEFAULT 0, " + - LAST_SESSION_RESET + " BLOB DEFAULT NULL, " + - WALLPAPER + " BLOB DEFAULT NULL, " + - WALLPAPER_URI + " TEXT DEFAULT NULL, " + - ABOUT + " TEXT DEFAULT NULL, " + - ABOUT_EMOJI + " TEXT DEFAULT NULL, " + - EXTRAS + " BLOB DEFAULT NULL, " + - GROUPS_IN_COMMON + " INTEGER DEFAULT 0, " + - CHAT_COLORS + " BLOB DEFAULT NULL, " + - CUSTOM_CHAT_COLORS_ID + " INTEGER DEFAULT 0, " + - BADGES + " BLOB DEFAULT NULL);"; - - private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID + - " FROM " + TABLE_NAME + - " INNER JOIN " + ThreadDatabase.TABLE_NAME + - " ON " + TABLE_NAME + "." + ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + - " WHERE " + - TABLE_NAME + "." + GROUP_ID + " IS NULL AND " + - TABLE_NAME + "." + REGISTERED + " = " + RegisteredState.NOT_REGISTERED.id + " AND " + - TABLE_NAME + "." + SEEN_INVITE_REMINDER + " < " + InsightsBannerTier.TIER_TWO.id + " AND " + - ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.HAS_SENT + " AND " + - ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.DATE + " > ?" + - " ORDER BY " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.DATE + " DESC LIMIT 50"; - - public RecipientDatabase(Context context, SignalDatabase databaseHelper) { - super(context, databaseHelper); - } - - public @NonNull boolean containsPhoneOrUuid(@NonNull String id) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String query = ACI_COLUMN + " = ? OR " + PHONE + " = ?"; - String[] args = new String[]{id, id}; - - try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID }, query, args, null, null, null)) { - return cursor != null && cursor.moveToFirst(); - } - } - - public @NonNull - Optional getByE164(@NonNull String e164) { - return getByColumn(PHONE, e164); - } - - public @NonNull - Optional getByEmail(@NonNull String email) { - return getByColumn(EMAIL, email); - } - - public @NonNull Optional getByGroupId(@NonNull GroupId groupId) { - return getByColumn(GROUP_ID, groupId.toString()); - } - - public @NonNull - Optional getByAci(@NonNull ACI uuid) { - return getByColumn(ACI_COLUMN, uuid.toString()); - } - - public @NonNull - Optional getByUsername(@NonNull String username) { - return getByColumn(USERNAME, username); - } - - public @NonNull RecipientId getAndPossiblyMerge(@Nullable ACI aci, @Nullable String e164, boolean highTrust) { - return getAndPossiblyMerge(aci, e164, highTrust, false); - } - - public @NonNull RecipientId getAndPossiblyMerge(@Nullable ACI aci, @Nullable String e164, boolean highTrust, boolean changeSelf) { - if (aci == null && e164 == null) { - throw new IllegalArgumentException("Must provide a UUID or E164!"); - } - - RecipientId recipientNeedingRefresh = null; - Pair remapped = null; - RecipientId recipientChangedNumber = null; - boolean transactionSuccessful = false; - - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.beginTransaction(); - - try { - Optional byE164 = e164 != null ? getByE164(e164) : Optional.absent(); - Optional byAci = aci != null ? getByAci(aci) : Optional.absent(); - - RecipientId finalId; - - if (!byE164.isPresent() && !byAci.isPresent()) { - Log.i(TAG, "Discovered a completely new user. Inserting.", true); - if (highTrust) { - long id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(e164, aci)); - finalId = RecipientId.from(id); - } else { - long id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(aci == null ? e164 : null, aci)); - finalId = RecipientId.from(id); - } - } else if (byE164.isPresent() && !byAci.isPresent()) { - if (aci != null) { - RecipientSettings e164Settings = getRecipientSettings(byE164.get()); - if (e164Settings.aci != null) { - if (highTrust) { - Log.w(TAG, String.format(Locale.US, "Found out about an ACI (%s) for a known E164 user (%s), but that user already has an ACI (%s). Likely a case of re-registration. High-trust, so stripping the E164 from the existing account and assigning it to a new entry.", aci, byE164.get(), e164Settings.aci), true); - - removePhoneNumber(byE164.get(), db); - recipientNeedingRefresh = byE164.get(); - - ContentValues insertValues = buildContentValuesForNewUser(e164, aci); - insertValues.put(BLOCKED, e164Settings.blocked ? 1 : 0); - - long id = db.insert(TABLE_NAME, null, insertValues); - finalId = RecipientId.from(id); - } else { - Log.w(TAG, String.format(Locale.US, "Found out about an ACI (%s) for a known E164 user (%s), but that user already has an ACI (%s). Likely a case of re-registration. Low-trust, so making a new user for the UUID.", aci, byE164.get(), e164Settings.aci), true); - - long id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(null, aci)); - finalId = RecipientId.from(id); - } - } else { - if (highTrust) { - Log.i(TAG, String.format(Locale.US, "Found out about an ACI (%s) for a known E164 user (%s). High-trust, so updating.", aci, byE164.get()), true); - markRegisteredOrThrow(byE164.get(), aci); - finalId = byE164.get(); - } else { - Log.i(TAG, String.format(Locale.US, "Found out about an ACI (%s) for a known E164 user (%s). Low-trust, so making a new user for the ACI.", aci, byE164.get()), true); - long id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(null, aci)); - finalId = RecipientId.from(id); - } - } - } else { - finalId = byE164.get(); - } - } else if (!byE164.isPresent() && byAci.isPresent()) { - if (e164 != null) { - if (highTrust) { - if (Objects.equals(aci, SignalStore.account().getAci()) && !changeSelf) { - Log.w(TAG, String.format(Locale.US, "Found out about an E164 (%s) for our own ACI user (%s). High-trust but not change self, doing nothing.", e164, byAci.get()), true); - finalId = byAci.get(); - } else { - Log.i(TAG, String.format(Locale.US, "Found out about an E164 (%s) for a known ACI user (%s). High-trust, so updating.", e164, byAci.get()), true); - - RecipientSettings byUuidSettings = getRecipientSettings(byAci.get()); - - setPhoneNumberOrThrow(byAci.get(), e164); - finalId = byAci.get(); - - if (!Util.isEmpty(byUuidSettings.e164) && !byUuidSettings.e164.equals(e164)) { - recipientChangedNumber = finalId; - } - } - } else { - Log.i(TAG, String.format(Locale.US, "Found out about an E164 (%s) for a known ACI user (%s). Low-trust, so doing nothing.", e164, byAci.get()), true); - finalId = byAci.get(); - } - } else { - finalId = byAci.get(); - } - } else { - if (byE164.equals(byAci)) { - finalId = byAci.get(); - } else { - Log.w(TAG, String.format(Locale.US, "Hit a conflict between %s (E164 of %s) and %s (ACI %s). They map to different recipients.", byE164.get(), e164, byAci.get(), aci), new Throwable(), true); - - RecipientSettings e164Settings = getRecipientSettings(byE164.get()); - - if (e164Settings.getAci() != null) { - if (highTrust) { - Log.w(TAG, "The E164 contact has a different ACI. Likely a case of re-registration. High-trust, so stripping the E164 from the existing account and assigning it to the ACI entry.", true); - - removePhoneNumber(byE164.get(), db); - recipientNeedingRefresh = byE164.get(); - - RecipientSettings byUuidSettings = getRecipientSettings(byAci.get()); - - setPhoneNumberOrThrow(byAci.get(), Objects.requireNonNull(e164)); - finalId = byAci.get(); - - if (!Util.isEmpty(byUuidSettings.e164) && !byUuidSettings.e164.equals(e164)) { - recipientChangedNumber = finalId; - } - } else { - Log.w(TAG, "The E164 contact has a different ACI. Likely a case of re-registration. Low-trust, so doing nothing.", true); - finalId = byAci.get(); - } - } else { - RecipientSettings aciSettings = getRecipientSettings(byAci.get()); - - if (aciSettings.getE164() != null) { - if (highTrust) { - Log.w(TAG, "We have one contact with just an E164, and another with both an ACI and a different E164. High-trust, so merging the two rows together. The E164 has also effectively changed for the ACI contact.", true); - finalId = merge(byAci.get(), byE164.get()); - recipientNeedingRefresh = byAci.get(); - remapped = new Pair<>(byE164.get(), byAci.get()); - recipientChangedNumber = finalId; - } else { - Log.w(TAG, "We have one contact with just an E164, and another with both an ACI and a different E164. Low-trust, so doing nothing.", true); - finalId = byAci.get(); - } - } else { - if (highTrust) { - Log.w(TAG, "We have one contact with just an E164, and another with just an ACI. High-trust, so merging the two rows together.", true); - finalId = merge(byAci.get(), byE164.get()); - recipientNeedingRefresh = byAci.get(); - remapped = new Pair<>(byE164.get(), byAci.get()); - } else { - Log.w(TAG, "We have one contact with just an E164, and another with just an ACI. Low-trust, so doing nothing.", true); - finalId = byAci.get(); - } - } - } - } - } - - db.setTransactionSuccessful(); - transactionSuccessful = true; - return finalId; - } finally { - db.endTransaction(); - - if (transactionSuccessful) { - if (recipientNeedingRefresh != null) { - Recipient.live(recipientNeedingRefresh).refresh(); - RetrieveProfileJob.enqueue(recipientNeedingRefresh); - } - - if (remapped != null) { - Recipient.live(remapped.first()).refresh(remapped.second()); - ApplicationDependencies.getRecipientCache().remap(remapped.first(), remapped.second()); - } - - if (recipientNeedingRefresh != null || remapped != null) { - StorageSyncHelper.scheduleSyncForDataChange(); - RecipientId.clearCache(); - } - - if (recipientChangedNumber != null) { - ApplicationDependencies.getJobManager().add(new RecipientChangedNumberJob(recipientChangedNumber)); - } - } - } - } - - private static ContentValues buildContentValuesForNewUser(@Nullable String e164, @Nullable ACI aci) { - ContentValues values = new ContentValues(); - - values.put(PHONE, e164); - - if (aci != null) { - values.put(ACI_COLUMN, aci.toString().toLowerCase()); - values.put(REGISTERED, RegisteredState.REGISTERED.getId()); - values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())); - values.put(AVATAR_COLOR, AvatarColor.random().serialize()); - } - - return values; - } - - - public @NonNull RecipientId getOrInsertFromAci(@NonNull ACI aci) { - return getOrInsertByColumn(ACI_COLUMN, aci.toString()).recipientId; - } - - public @NonNull RecipientId getOrInsertFromE164(@NonNull String e164) { - return getOrInsertByColumn(PHONE, e164).recipientId; - } - - public @NonNull RecipientId getOrInsertFromEmail(@NonNull String email) { - return getOrInsertByColumn(EMAIL, email).recipientId; - } - - public @NonNull RecipientId getOrInsertFromGroupId(@NonNull GroupId groupId) { - Optional existing = getByGroupId(groupId); - - if (existing.isPresent()) { - return existing.get(); - } else if (groupId.isV1() && SignalDatabase.groups().groupExists(groupId.requireV1().deriveV2MigrationGroupId())) { - throw new GroupDatabase.LegacyGroupInsertException(groupId); - } else if (groupId.isV2() && SignalDatabase.groups().getGroupV1ByExpectedV2(groupId.requireV2()).isPresent()) { - throw new GroupDatabase.MissedGroupMigrationInsertException(groupId); - } else { - ContentValues values = new ContentValues(); - values.put(GROUP_ID, groupId.toString()); - values.put(AVATAR_COLOR, AvatarColor.random().serialize()); - - long id = databaseHelper.getSignalWritableDatabase().insert(TABLE_NAME, null, values); - - if (id < 0) { - existing = getByColumn(GROUP_ID, groupId.toString()); - - if (existing.isPresent()) { - return existing.get(); - } else if (groupId.isV1() && SignalDatabase.groups().groupExists(groupId.requireV1().deriveV2MigrationGroupId())) { - throw new GroupDatabase.LegacyGroupInsertException(groupId); - } else if (groupId.isV2() && SignalDatabase.groups().getGroupV1ByExpectedV2(groupId.requireV2()).isPresent()) { - throw new GroupDatabase.MissedGroupMigrationInsertException(groupId); - } else { - throw new AssertionError("Failed to insert recipient!"); - } - } else { - ContentValues groupUpdates = new ContentValues(); - - if (groupId.isMms()) { - groupUpdates.put(GROUP_TYPE, GroupType.MMS.getId()); - } else { - if (groupId.isV2()) { - groupUpdates.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId()); - } else { - groupUpdates.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId()); - } - groupUpdates.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())); - } - - RecipientId recipientId = RecipientId.from(id); - - update(recipientId, groupUpdates); - - return recipientId; - } - } - } - - /** - * See {@link Recipient#externalPossiblyMigratedGroup(Context, GroupId)}. - */ - public @NonNull RecipientId getOrInsertFromPossiblyMigratedGroupId(@NonNull GroupId groupId) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - try { - Optional existing = getByColumn(GROUP_ID, groupId.toString()); - - if (existing.isPresent()) { - db.setTransactionSuccessful(); - return existing.get(); - } - - if (groupId.isV1()) { - Optional v2 = getByGroupId(groupId.requireV1().deriveV2MigrationGroupId()); - if (v2.isPresent()) { - db.setTransactionSuccessful(); - return v2.get(); - } - } - - if (groupId.isV2()) { - Optional v1 = SignalDatabase.groups().getGroupV1ByExpectedV2(groupId.requireV2()); - if (v1.isPresent()) { - db.setTransactionSuccessful(); - return v1.get().getRecipientId(); - } - } - - RecipientId id = getOrInsertFromGroupId(groupId); - - db.setTransactionSuccessful(); - return id; - } finally { - db.endTransaction(); - } - } - - public Cursor getBlocked() { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - - return database.query(TABLE_NAME, ID_PROJECTION, BLOCKED + " = 1", - null, null, null, null, null); - } - - public RecipientReader readerForBlocked(Cursor cursor) { - return new RecipientReader(cursor); - } - - public RecipientReader getRecipientsWithNotificationChannels() { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - Cursor cursor = database.query(TABLE_NAME, ID_PROJECTION, NOTIFICATION_CHANNEL + " NOT NULL", - null, null, null, null, null); - - return new RecipientReader(cursor); - } - - public @NonNull RecipientSettings getRecipientSettings(@NonNull RecipientId id) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - String query = ID + " = ?"; - String[] args = new String[] { id.serialize() }; - - try (Cursor cursor = database.query(TABLE_NAME, RECIPIENT_PROJECTION, query, args, null, null, null)) { - if (cursor != null && cursor.moveToNext()) { - return getRecipientSettings(context, cursor); - } else { - Optional remapped = RemappedRecords.getInstance().getRecipient(id); - if (remapped.isPresent()) { - Log.w(TAG, "Missing recipient for " + id + ", but found it in the remapped records as " + remapped.get()); - return getRecipientSettings(remapped.get()); - } else { - throw new MissingRecipientException(id); - } - } - } - } - - public @Nullable RecipientSettings getRecipientSettingsForSync(@NonNull RecipientId id) { - String query = TABLE_NAME + "." + ID + " = ?"; - String[] args = new String[]{id.serialize()}; - - List recipientSettingsForSync = getRecipientSettingsForSync(query, args); - - if (recipientSettingsForSync.isEmpty()) { - return null; - } - - if (recipientSettingsForSync.size() > 1) { - throw new AssertionError(); - } - - return recipientSettingsForSync.get(0); - } - - public @Nullable RecipientSettings getByStorageId(@NonNull byte[] storageId) { - List result = getRecipientSettingsForSync(TABLE_NAME + "." + STORAGE_SERVICE_ID + " = ?", new String[] { Base64.encodeBytes(storageId) }); - - if (result.size() > 0) { - return result.get(0); - } - - return null; - } - - public void markNeedsSyncWithoutRefresh(@NonNull Collection recipientIds) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - try { - for (RecipientId recipientId : recipientIds) { - rotateStorageId(recipientId); - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - public void markNeedsSync(@NonNull RecipientId recipientId) { - rotateStorageId(recipientId); - Recipient.live(recipientId).refresh(); - } - - public void applyStorageIdUpdates(@NonNull Map storageIds) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - try { - String query = ID + " = ?"; - - for (Map.Entry entry : storageIds.entrySet()) { - ContentValues values = new ContentValues(); - values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(entry.getValue().getRaw())); - - db.update(TABLE_NAME, values, query, new String[] { entry.getKey().serialize() }); - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - for (RecipientId id : storageIds.keySet()) { - Recipient.live(id).refresh(); - } - } - - public void applyStorageSyncContactInsert(@NonNull SignalContactRecord insert) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - ThreadDatabase threadDatabase = SignalDatabase.threads(); - - ContentValues values = getValuesForStorageContact(insert, true); - long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE); - RecipientId recipientId = null; - - if (id < 0) { - Log.w(TAG, "[applyStorageSyncContactInsert] Failed to insert. Possibly merging."); - recipientId = getAndPossiblyMerge(insert.getAddress().hasValidAci() ? insert.getAddress().getAci() : null, insert.getAddress().getNumber().orNull(), true); - db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId)); - } else { - recipientId = RecipientId.from(id); - } - - if (insert.getIdentityKey().isPresent() && insert.getAddress().hasValidAci()) { - try { - IdentityKey identityKey = new IdentityKey(insert.getIdentityKey().get(), 0); - - SignalDatabase.identities().updateIdentityAfterSync(insert.getAddress().getIdentifier(), recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.getIdentityState())); - } catch (InvalidKeyException e) { - Log.w(TAG, "Failed to process identity key during insert! Skipping.", e); - } - } - - threadDatabase.applyStorageSyncUpdate(recipientId, insert); - } - - public void applyStorageSyncContactUpdate(@NonNull StorageRecordUpdate update) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getIdentityStore(); - ContentValues values = getValuesForStorageContact(update.getNew(), false); - - 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, "[applyStorageSyncContactUpdate] Failed to update a user by storageId."); - - RecipientId recipientId = getByColumn(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getOld().getId().getRaw())).get(); - Log.w(TAG, "[applyStorageSyncContactUpdate] Found user " + recipientId + ". Possibly merging."); - - recipientId = getAndPossiblyMerge(update.getNew().getAddress().hasValidAci() ? update.getNew().getAddress().getAci() : null, update.getNew().getAddress().getNumber().orNull(), true); - Log.w(TAG, "[applyStorageSyncContactUpdate] Merged into " + recipientId); - - db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId)); - } - - RecipientId recipientId = getByStorageKeyOrThrow(update.getNew().getId().getRaw()); - - if (StorageSyncHelper.profileKeyChanged(update)) { - ContentValues clearValues = new ContentValues(1); - clearValues.putNull(PROFILE_KEY_CREDENTIAL); - db.update(TABLE_NAME, clearValues, ID_WHERE, SqlUtil.buildArgs(recipientId)); - } - - try { - Optional oldIdentityRecord = identityStore.getIdentityRecord(recipientId); - - if (update.getNew().getIdentityKey().isPresent() && update.getNew().getAddress().hasValidAci()) { - IdentityKey identityKey = new IdentityKey(update.getNew().getIdentityKey().get(), 0); - SignalDatabase.identities().updateIdentityAfterSync(update.getNew().getAddress().getIdentifier(), recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(update.getNew().getIdentityState())); - } - - Optional newIdentityRecord = identityStore.getIdentityRecord(recipientId); - - if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED) && - (!oldIdentityRecord.isPresent() || oldIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED)) - { - IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), true, true); - } else if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED) && - (oldIdentityRecord.isPresent() && oldIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED)) - { - IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), false, true); - } - } catch (InvalidKeyException e) { - Log.w(TAG, "Failed to process identity key during update! Skipping.", e); - } - - SignalDatabase.threads().applyStorageSyncUpdate(recipientId, update.getNew()); - - Recipient.live(recipientId).refresh(); - } - - public void applyStorageSyncGroupV1Insert(@NonNull SignalGroupV1Record insert) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - long id = db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert, true)); - RecipientId recipientId = RecipientId.from(id); - - SignalDatabase.threads().applyStorageSyncUpdate(recipientId, insert); - - Recipient.live(recipientId).refresh(); - } - - public void applyStorageSyncGroupV1Update(@NonNull StorageRecordUpdate update) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - ContentValues values = getValuesForStorageGroupV1(update.getNew(), false); - 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!"); - } - - Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(update.getOld().getGroupId())); - - SignalDatabase.threads().applyStorageSyncUpdate(recipient.getId(), update.getNew()); - - recipient.live().refresh(); - } - - public void applyStorageSyncGroupV2Insert(@NonNull SignalGroupV2Record insert) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - GroupMasterKey masterKey = insert.getMasterKeyOrThrow(); - GroupId.V2 groupId = GroupId.v2(masterKey); - ContentValues values = getValuesForStorageGroupV2(insert, true); - long id = db.insertOrThrow(TABLE_NAME, null, values); - Recipient recipient = Recipient.externalGroupExact(context, groupId); - - Log.i(TAG, "Creating restore placeholder for " + groupId); - SignalDatabase.groups() - .create(masterKey, - DecryptedGroup.newBuilder() - .setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) - .build()); - - Log.i(TAG, "Scheduling request for latest group info for " + groupId); - - ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId)); - - SignalDatabase.threads().applyStorageSyncUpdate(recipient.getId(), insert); - - recipient.live().refresh(); - } - - public void applyStorageSyncGroupV2Update(@NonNull StorageRecordUpdate update) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - ContentValues values = getValuesForStorageGroupV2(update.getNew(), false); - 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!"); - } - - GroupMasterKey masterKey = update.getOld().getMasterKeyOrThrow(); - Recipient recipient = Recipient.externalGroupExact(context, GroupId.v2(masterKey)); - - SignalDatabase.threads().applyStorageSyncUpdate(recipient.getId(), update.getNew()); - - recipient.live().refresh(); - } - - public void applyStorageSyncAccountUpdate(@NonNull StorageRecordUpdate update) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - ContentValues values = new ContentValues(); - ProfileName profileName = ProfileName.fromParts(update.getNew().getGivenName().orNull(), update.getNew().getFamilyName().orNull()); - Optional localKey = ProfileKeyUtil.profileKeyOptional(update.getOld().getProfileKey().orNull()); - Optional remoteKey = ProfileKeyUtil.profileKeyOptional(update.getNew().getProfileKey().orNull()); - String profileKey = remoteKey.or(localKey).transform(ProfileKey::serialize).transform(Base64::encodeBytes).orNull(); - - if (!remoteKey.isPresent()) { - Log.w(TAG, "Got an empty profile key while applying an account record update!"); - } - - values.put(PROFILE_GIVEN_NAME, profileName.getGivenName()); - values.put(PROFILE_FAMILY_NAME, profileName.getFamilyName()); - values.put(PROFILE_JOINED_NAME, profileName.toString()); - values.put(PROFILE_KEY, profileKey); - values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getNew().getId().getRaw())); - - if (update.getNew().hasUnknownFields()) { - values.put(STORAGE_PROTO, Base64.encodeBytes(Objects.requireNonNull(update.getNew().serializeUnknownFields()))); - } else { - values.putNull(STORAGE_PROTO); - } - - int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())}); - if (updateCount < 1) { - throw new AssertionError("Account update didn't match any rows!"); - } - - if (!remoteKey.equals(localKey)) { - ApplicationDependencies.getJobManager().add(new RefreshAttributesJob()); - } - - SignalDatabase.threads().applyStorageSyncUpdate(Recipient.self().getId(), update.getNew()); - - Recipient.self().live().refresh(); - } - - public void updatePhoneNumbers(@NonNull Map mapping) { - if (mapping.isEmpty()) return; - - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - try { - String query = PHONE + " = ?"; - - for (Map.Entry entry : mapping.entrySet()) { - ContentValues values = new ContentValues(); - values.put(PHONE, entry.getValue()); - - db.updateWithOnConflict(TABLE_NAME, values, query, new String[] { entry.getKey() }, SQLiteDatabase.CONFLICT_IGNORE); - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - private @NonNull RecipientId getByStorageKeyOrThrow(byte[] storageKey) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String query = STORAGE_SERVICE_ID + " = ?"; - String[] args = new String[]{Base64.encodeBytes(storageKey)}; - - try (Cursor cursor = db.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); - return RecipientId.from(id); - } else { - throw new AssertionError("No recipient with that storage key!"); - } - } - } - - private static @NonNull ContentValues getValuesForStorageContact(@NonNull SignalContactRecord contact, boolean isInsert) { - ContentValues values = new ContentValues(); - - ProfileName profileName = ProfileName.fromParts(contact.getGivenName().orNull(), contact.getFamilyName().orNull()); - String username = contact.getUsername().orNull(); - - if (contact.getAddress().hasValidAci()) { - values.put(ACI_COLUMN, contact.getAddress().getAci().toString()); - } - - values.put(PHONE, contact.getAddress().getNumber().orNull()); - values.put(PROFILE_GIVEN_NAME, profileName.getGivenName()); - values.put(PROFILE_FAMILY_NAME, profileName.getFamilyName()); - values.put(PROFILE_JOINED_NAME, profileName.toString()); - values.put(PROFILE_KEY, contact.getProfileKey().transform(Base64::encodeBytes).orNull()); - values.put(USERNAME, TextUtils.isEmpty(username) ? null : username); - values.put(PROFILE_SHARING, contact.isProfileSharingEnabled() ? "1" : "0"); - values.put(BLOCKED, contact.isBlocked() ? "1" : "0"); - values.put(MUTE_UNTIL, contact.getMuteUntil()); - values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(contact.getId().getRaw())); - - if (contact.hasUnknownFields()) { - values.put(STORAGE_PROTO, Base64.encodeBytes(Objects.requireNonNull(contact.serializeUnknownFields()))); - } else { - values.putNull(STORAGE_PROTO); - } - - if (isInsert) { - values.put(AVATAR_COLOR, AvatarColor.random().serialize()); - } - - return values; - } - - private static @NonNull ContentValues getValuesForStorageGroupV1(@NonNull SignalGroupV1Record groupV1, boolean isInsert) { - ContentValues values = new ContentValues(); - values.put(GROUP_ID, GroupId.v1orThrow(groupV1.getGroupId()).toString()); - values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId()); - values.put(PROFILE_SHARING, groupV1.isProfileSharingEnabled() ? "1" : "0"); - values.put(BLOCKED, groupV1.isBlocked() ? "1" : "0"); - values.put(MUTE_UNTIL, groupV1.getMuteUntil()); - values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV1.getId().getRaw())); - - if (groupV1.hasUnknownFields()) { - values.put(STORAGE_PROTO, Base64.encodeBytes(groupV1.serializeUnknownFields())); - } else { - values.putNull(STORAGE_PROTO); - } - - if (isInsert) { - values.put(AVATAR_COLOR, AvatarColor.random().serialize()); - } - - return values; - } - - private static @NonNull ContentValues getValuesForStorageGroupV2(@NonNull SignalGroupV2Record groupV2, boolean isInsert) { - ContentValues values = new ContentValues(); - values.put(GROUP_ID, GroupId.v2(groupV2.getMasterKeyOrThrow()).toString()); - values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId()); - values.put(PROFILE_SHARING, groupV2.isProfileSharingEnabled() ? "1" : "0"); - values.put(BLOCKED, groupV2.isBlocked() ? "1" : "0"); - values.put(MUTE_UNTIL, groupV2.getMuteUntil()); - values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV2.getId().getRaw())); - - if (groupV2.hasUnknownFields()) { - values.put(STORAGE_PROTO, Base64.encodeBytes(groupV2.serializeUnknownFields())); - } else { - values.putNull(STORAGE_PROTO); - } - - if (isInsert) { - values.put(AVATAR_COLOR, AvatarColor.random().serialize()); - } - - return values; - } - - private List getRecipientSettingsForSync(@Nullable String query, @Nullable String[] args) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ACI_COLUMN + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.ADDRESS - + " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + GROUP_ID + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID - + " LEFT OUTER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID; - List out = new ArrayList<>(); - - String[] columns = Stream.of(TYPED_RECIPIENT_PROJECTION, - new String[]{ RecipientDatabase.TABLE_NAME + "." + STORAGE_PROTO, - GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY, - ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ARCHIVED, - ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.READ, - IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS, - IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY }) - .flatMap(Stream::of) - .toArray(String[]::new); - - try (Cursor cursor = db.query(table, columns, query, args, TABLE_NAME + "." + ID, null, null)) { - while (cursor != null && cursor.moveToNext()) { - out.add(getRecipientSettings(context, cursor)); - } - } - - return out; - } - - /** - * @return All storage ids for ContactRecords, excluding the ones that need to be deleted. - */ - public List getContactStorageSyncIds() { - return new ArrayList<>(getContactStorageSyncIdsMap().values()); - } - - /** - * @return All storage IDs for ContactRecords, excluding the ones that need to be deleted. - */ - public @NonNull Map getContactStorageSyncIdsMap() { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String query = STORAGE_SERVICE_ID + " NOT NULL AND " + ACI_COLUMN + " NOT NULL AND " + ID + " != ? AND " + GROUP_TYPE + " != ?"; - String[] args = SqlUtil.buildArgs(Recipient.self().getId(), String.valueOf(GroupType.SIGNAL_V2.getId())); - Map out = new HashMap<>(); - - try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID, STORAGE_SERVICE_ID, GROUP_TYPE }, query, args, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - RecipientId id = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID))); - String encodedKey = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_SERVICE_ID)); - GroupType groupType = GroupType.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(GROUP_TYPE))); - byte[] key = Base64.decodeOrThrow(encodedKey); - - switch (groupType) { - case NONE : out.put(id, StorageId.forContact(key)); break; - case SIGNAL_V1 : out.put(id, StorageId.forGroupV1(key)); break; - default : throw new AssertionError(); - } - } - } - - for (GroupId.V2 id : SignalDatabase.groups().getAllGroupV2Ids()) { - Recipient recipient = Recipient.externalGroupExact(context, id); - RecipientId recipientId = recipient.getId(); - RecipientSettings recipientSettingsForSync = getRecipientSettingsForSync(recipientId); - - if (recipientSettingsForSync == null) { - throw new AssertionError(); - } - - byte[] key = recipientSettingsForSync.storageId; - - if (key == null) { - throw new AssertionError(); - } - - out.put(recipientId, StorageId.forGroupV2(key)); - } - - return out; - } - - static @NonNull RecipientSettings getRecipientSettings(@NonNull Context context, @NonNull Cursor cursor) { - return getRecipientSettings(context, cursor, ID); - } - - static @NonNull RecipientSettings getRecipientSettings(@NonNull Context context, @NonNull Cursor cursor, @NonNull String idColumnName) { - long id = CursorUtil.requireLong(cursor, idColumnName); - ACI uuid = ACI.parseOrNull(CursorUtil.requireString(cursor, ACI_COLUMN)); - String username = CursorUtil.requireString(cursor, USERNAME); - String e164 = CursorUtil.requireString(cursor, PHONE); - String email = CursorUtil.requireString(cursor, EMAIL); - GroupId groupId = GroupId.parseNullableOrThrow(CursorUtil.requireString(cursor, GROUP_ID)); - int groupType = CursorUtil.requireInt(cursor, GROUP_TYPE); - boolean blocked = CursorUtil.requireBoolean(cursor, BLOCKED); - String messageRingtone = CursorUtil.requireString(cursor, MESSAGE_RINGTONE); - String callRingtone = CursorUtil.requireString(cursor, CALL_RINGTONE); - int messageVibrateState = CursorUtil.requireInt(cursor, MESSAGE_VIBRATE); - int callVibrateState = CursorUtil.requireInt(cursor, CALL_VIBRATE); - long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL)); - int insightsBannerTier = CursorUtil.requireInt(cursor, SEEN_INVITE_REMINDER); - int defaultSubscriptionId = CursorUtil.requireInt(cursor, DEFAULT_SUBSCRIPTION_ID); - int expireMessages = CursorUtil.requireInt(cursor, MESSAGE_EXPIRATION_TIME); - int registeredState = CursorUtil.requireInt(cursor, REGISTERED); - String profileKeyString = CursorUtil.requireString(cursor, PROFILE_KEY); - String profileKeyCredentialString = CursorUtil.requireString(cursor, PROFILE_KEY_CREDENTIAL); - String systemGivenName = CursorUtil.requireString(cursor, SYSTEM_GIVEN_NAME); - String systemFamilyName = CursorUtil.requireString(cursor, SYSTEM_FAMILY_NAME); - String systemDisplayName = CursorUtil.requireString(cursor, SYSTEM_JOINED_NAME); - String systemContactPhoto = CursorUtil.requireString(cursor, SYSTEM_PHOTO_URI); - String systemPhoneLabel = CursorUtil.requireString(cursor, SYSTEM_PHONE_LABEL); - String systemContactUri = CursorUtil.requireString(cursor, SYSTEM_CONTACT_URI); - String profileGivenName = CursorUtil.requireString(cursor, PROFILE_GIVEN_NAME); - String profileFamilyName = CursorUtil.requireString(cursor, PROFILE_FAMILY_NAME); - String signalProfileAvatar = CursorUtil.requireString(cursor, SIGNAL_PROFILE_AVATAR); - boolean profileSharing = CursorUtil.requireBoolean(cursor, PROFILE_SHARING); - long lastProfileFetch = cursor.getLong(cursor.getColumnIndexOrThrow(LAST_PROFILE_FETCH)); - String notificationChannel = CursorUtil.requireString(cursor, NOTIFICATION_CHANNEL); - int unidentifiedAccessMode = CursorUtil.requireInt(cursor, UNIDENTIFIED_ACCESS_MODE); - boolean forceSmsSelection = CursorUtil.requireBoolean(cursor, FORCE_SMS_SELECTION); - long capabilities = CursorUtil.requireLong(cursor, CAPABILITIES); - String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID); - int mentionSettingId = CursorUtil.requireInt(cursor, MENTION_SETTING); - byte[] wallpaper = CursorUtil.requireBlob(cursor, WALLPAPER); - byte[] serializedChatColors = CursorUtil.requireBlob(cursor, CHAT_COLORS); - long customChatColorsId = CursorUtil.requireLong(cursor, CUSTOM_CHAT_COLORS_ID); - String serializedAvatarColor = CursorUtil.requireString(cursor, AVATAR_COLOR); - String about = CursorUtil.requireString(cursor, ABOUT); - String aboutEmoji = CursorUtil.requireString(cursor, ABOUT_EMOJI); - boolean hasGroupsInCommon = CursorUtil.requireBoolean(cursor, GROUPS_IN_COMMON); - byte[] serializedBadgeList = CursorUtil.requireBlob(cursor, BADGES); - - List badges = parseBadgeList(serializedBadgeList); - - byte[] profileKey = null; - ProfileKeyCredential profileKeyCredential = null; - - if (profileKeyString != null) { - try { - profileKey = Base64.decode(profileKeyString); - } catch (IOException e) { - Log.w(TAG, e); - profileKey = null; - } - - if (profileKeyCredentialString != null) { - try { - byte[] columnDataBytes = Base64.decode(profileKeyCredentialString); - - ProfileKeyCredentialColumnData columnData = ProfileKeyCredentialColumnData.parseFrom(columnDataBytes); - - if (Arrays.equals(columnData.getProfileKey().toByteArray(), profileKey)) { - profileKeyCredential = new ProfileKeyCredential(columnData.getProfileKeyCredential().toByteArray()); - } else { - Log.i(TAG, "Out of date profile key credential data ignored on read"); - } - } catch (InvalidInputException | IOException e) { - Log.w(TAG, "Profile key credential column data could not be read", e); - } - } - } - - byte[] storageKey = storageKeyRaw != null ? Base64.decodeOrThrow(storageKeyRaw) : null; - - ChatWallpaper chatWallpaper = null; - - if (wallpaper != null) { - try { - chatWallpaper = ChatWallpaperFactory.create(Wallpaper.parseFrom(wallpaper)); - } catch (InvalidProtocolBufferException e) { - Log.w(TAG, "Failed to parse wallpaper.", e); - } - } - - ChatColors chatColors = null; - if (serializedChatColors != null) { - try { - chatColors = ChatColors.forChatColor(ChatColors.Id.forLongValue(customChatColorsId), ChatColor.parseFrom(serializedChatColors)); - } catch (InvalidProtocolBufferException e) { - Log.w(TAG, "Failed to parse chat colors.", e); - } - } - - return new RecipientSettings(RecipientId.from(id), - uuid, - username, - e164, - email, - groupId, - GroupType.fromId(groupType), - blocked, - muteUntil, - VibrateState.fromId(messageVibrateState), - VibrateState.fromId(callVibrateState), - Util.uri(messageRingtone), - Util.uri(callRingtone), - defaultSubscriptionId, - expireMessages, - RegisteredState.fromId(registeredState), - profileKey, - profileKeyCredential, - ProfileName.fromParts(systemGivenName, systemFamilyName), - systemDisplayName, - systemContactPhoto, - systemPhoneLabel, - systemContactUri, - ProfileName.fromParts(profileGivenName, profileFamilyName), - signalProfileAvatar, - AvatarHelper.hasAvatar(context, RecipientId.from(id)), - profileSharing, - lastProfileFetch, - notificationChannel, - UnidentifiedAccessMode.fromMode(unidentifiedAccessMode), - forceSmsSelection, - capabilities, - InsightsBannerTier.fromId(insightsBannerTier), - storageKey, - MentionSetting.fromId(mentionSettingId), - chatWallpaper, - chatColors, - AvatarColor.deserialize(serializedAvatarColor), - about, - aboutEmoji, - getSyncExtras(cursor), - getExtras(cursor), - hasGroupsInCommon, - badges); - } - - private static @NonNull List parseBadgeList(byte[] serializedBadgeList) { - BadgeList badgeList = null; - if (serializedBadgeList != null) { - try { - badgeList = BadgeList.parseFrom(serializedBadgeList); - } catch (InvalidProtocolBufferException e) { - Log.w(TAG, e); - } - } - - List badges; - if (badgeList != null) { - List protoBadges = badgeList.getBadgesList(); - badges = new ArrayList<>(protoBadges.size()); - for (BadgeList.Badge protoBadge : protoBadges) { - badges.add(Badges.fromDatabaseBadge(protoBadge)); - } - } else { - badges = Collections.emptyList(); - } - - return badges; - } - - private static @NonNull RecipientSettings.SyncExtras getSyncExtras(@NonNull Cursor cursor) { - String storageProtoRaw = CursorUtil.getString(cursor, STORAGE_PROTO).orNull(); - byte[] storageProto = storageProtoRaw != null ? Base64.decodeOrThrow(storageProtoRaw) : null; - boolean archived = CursorUtil.getBoolean(cursor, ThreadDatabase.ARCHIVED).or(false); - boolean forcedUnread = CursorUtil.getInt(cursor, ThreadDatabase.READ).transform(status -> status == ThreadDatabase.ReadStatus.FORCED_UNREAD.serialize()).or(false); - GroupMasterKey groupMasterKey = CursorUtil.getBlob(cursor, GroupDatabase.V2_MASTER_KEY).transform(GroupUtil::requireMasterKey).orNull(); - byte[] identityKey = CursorUtil.getString(cursor, IDENTITY_KEY).transform(Base64::decodeOrThrow).orNull(); - VerifiedStatus identityStatus = CursorUtil.getInt(cursor, IDENTITY_STATUS).transform(VerifiedStatus::forState).or(VerifiedStatus.DEFAULT); - - - return new RecipientSettings.SyncExtras(storageProto, groupMasterKey, identityKey, identityStatus, archived, forcedUnread); - } - - private static @Nullable Recipient.Extras getExtras(@NonNull Cursor cursor) { - return Recipient.Extras.from(getRecipientExtras(cursor)); - } - - private static @Nullable RecipientExtras getRecipientExtras(@NonNull Cursor cursor) { - final Optional blob = CursorUtil.getBlob(cursor, EXTRAS); - - return blob.transform(b -> { - try { - return RecipientExtras.parseFrom(b); - } catch (InvalidProtocolBufferException e) { - Log.w(TAG, e); - throw new AssertionError(e); - } - }).orNull(); - } - - public BulkOperationsHandle beginBulkSystemContactUpdate() { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - database.beginTransaction(); - - ContentValues contentValues = new ContentValues(1); - contentValues.put(SYSTEM_INFO_PENDING, 1); - - database.update(TABLE_NAME, contentValues, SYSTEM_CONTACT_URI + " NOT NULL", null); - - return new BulkOperationsHandle(database); - } - - void onUpdatedChatColors(@NonNull ChatColors chatColors) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - String where = CUSTOM_CHAT_COLORS_ID + " = ?"; - String[] args = SqlUtil.buildArgs(chatColors.getId().getLongValue()); - List updated = new LinkedList<>(); - - try (Cursor cursor = database.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - updated.add(RecipientId.from(CursorUtil.requireLong(cursor, ID))); - } - } - - if (updated.isEmpty()) { - Log.d(TAG, "No recipients utilizing updated chat color."); - } else { - ContentValues values = new ContentValues(2); - - values.put(CHAT_COLORS, chatColors.serialize().toByteArray()); - values.put(CUSTOM_CHAT_COLORS_ID, chatColors.getId().getLongValue()); - - database.update(TABLE_NAME, values, where, args); - - for (RecipientId recipientId : updated) { - Recipient.live(recipientId).refresh(); - } - } - } - - void onDeletedChatColors(@NonNull ChatColors chatColors) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - String where = CUSTOM_CHAT_COLORS_ID + " = ?"; - String[] args = SqlUtil.buildArgs(chatColors.getId().getLongValue()); - List updated = new LinkedList<>(); - - try (Cursor cursor = database.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - updated.add(RecipientId.from(CursorUtil.requireLong(cursor, ID))); - } - } - - if (updated.isEmpty()) { - Log.d(TAG, "No recipients utilizing deleted chat color."); - } else { - ContentValues values = new ContentValues(2); - - values.put(CHAT_COLORS, (byte[]) null); - values.put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.INSTANCE.getLongValue()); - - database.update(TABLE_NAME, values, where, args); - - for (RecipientId recipientId : updated) { - Recipient.live(recipientId).refresh(); - } - } - } - - public int getColorUsageCount(@NotNull ChatColors.Id chatColorsId) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String[] projection = SqlUtil.buildArgs("COUNT(*)"); - String where = CUSTOM_CHAT_COLORS_ID + " = ?"; - String[] args = SqlUtil.buildArgs(chatColorsId.getLongValue()); - - try (Cursor cursor = db.query(TABLE_NAME, projection, where, args, null, null, null)) { - if (cursor == null) { - return 0; - } else { - cursor.moveToFirst(); - return cursor.getInt(0); - } - } - } - - public void clearAllColors() { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - String where = CUSTOM_CHAT_COLORS_ID + " != ?"; - String[] args = SqlUtil.buildArgs(ChatColors.Id.NotSet.INSTANCE.getLongValue()); - List toUpdate = new LinkedList<>(); - - try (Cursor cursor = database.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - toUpdate.add(RecipientId.from(CursorUtil.requireLong(cursor, ID))); - } - } - - if (toUpdate.isEmpty()) { - return; - } - - ContentValues values = new ContentValues(); - values.put(CHAT_COLORS, (byte[]) null); - values.put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.INSTANCE.getLongValue()); - - database.update(TABLE_NAME, values, where, args); - - for (RecipientId id : toUpdate) { - Recipient.live(id).refresh(); - } - } - - public void clearColor(@NonNull RecipientId id) { - ContentValues values = new ContentValues(); - values.put(CHAT_COLORS, (byte[]) null); - values.put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.INSTANCE.getLongValue()); - if (update(id, values)) { - Recipient.live(id).refresh(); - } - } - - public void setColor(@NonNull RecipientId id, @NonNull ChatColors color) { - ContentValues values = new ContentValues(); - values.put(CHAT_COLORS, color.serialize().toByteArray()); - values.put(CUSTOM_CHAT_COLORS_ID, color.getId().getLongValue()); - if (update(id, values)) { - Recipient.live(id).refresh(); - } - } - - public void setDefaultSubscriptionId(@NonNull RecipientId id, int defaultSubscriptionId) { - ContentValues values = new ContentValues(); - values.put(DEFAULT_SUBSCRIPTION_ID, defaultSubscriptionId); - if (update(id, values)) { - Recipient.live(id).refresh(); - } - } - - public void setForceSmsSelection(@NonNull RecipientId id, boolean forceSmsSelection) { - ContentValues contentValues = new ContentValues(1); - contentValues.put(FORCE_SMS_SELECTION, forceSmsSelection ? 1 : 0); - if (update(id, contentValues)) { - Recipient.live(id).refresh(); - } - } - - public void setBlocked(@NonNull RecipientId id, boolean blocked) { - ContentValues values = new ContentValues(); - values.put(BLOCKED, blocked ? 1 : 0); - if (update(id, values)) { - rotateStorageId(id); - Recipient.live(id).refresh(); - } - } - - public void setMessageRingtone(@NonNull RecipientId id, @Nullable Uri notification) { - ContentValues values = new ContentValues(); - values.put(MESSAGE_RINGTONE, notification == null ? null : notification.toString()); - if (update(id, values)) { - Recipient.live(id).refresh(); - } - } - - public void setCallRingtone(@NonNull RecipientId id, @Nullable Uri ringtone) { - ContentValues values = new ContentValues(); - values.put(CALL_RINGTONE, ringtone == null ? null : ringtone.toString()); - if (update(id, values)) { - Recipient.live(id).refresh(); - } - } - - public void setMessageVibrate(@NonNull RecipientId id, @NonNull VibrateState enabled) { - ContentValues values = new ContentValues(); - values.put(MESSAGE_VIBRATE, enabled.getId()); - if (update(id, values)) { - Recipient.live(id).refresh(); - } - } - - public void setCallVibrate(@NonNull RecipientId id, @NonNull VibrateState enabled) { - ContentValues values = new ContentValues(); - values.put(CALL_VIBRATE, enabled.getId()); - if (update(id, values)) { - Recipient.live(id).refresh(); - } - } - - public void setMuted(@NonNull RecipientId id, long until) { - ContentValues values = new ContentValues(); - values.put(MUTE_UNTIL, until); - if (update(id, values)) { - rotateStorageId(id); - Recipient.live(id).refresh(); - } - StorageSyncHelper.scheduleSyncForDataChange(); - } - - public void setMuted(@NonNull Collection ids, long until) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - try { - ContentValues values = new ContentValues(); - values.put(MUTE_UNTIL, until); - - SqlUtil.Query query = SqlUtil.buildCollectionQuery(ID, ids); - db.update(TABLE_NAME, values, query.getWhere(), query.getWhereArgs()); - - for (RecipientId id : ids) { - rotateStorageId(id); - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - for (RecipientId id : ids) { - Recipient.live(id).refresh(); - } - - StorageSyncHelper.scheduleSyncForDataChange(); - } - - public void setSeenFirstInviteReminder(@NonNull RecipientId id) { - setInsightsBannerTier(id, InsightsBannerTier.TIER_ONE); - } - - public void setSeenSecondInviteReminder(@NonNull RecipientId id) { - setInsightsBannerTier(id, InsightsBannerTier.TIER_TWO); - } - - public void setHasSentInvite(@NonNull RecipientId id) { - setSeenSecondInviteReminder(id); - } - - private void setInsightsBannerTier(@NonNull RecipientId id, @NonNull InsightsBannerTier insightsBannerTier) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - ContentValues values = new ContentValues(1); - String query = ID + " = ? AND " + SEEN_INVITE_REMINDER + " < ?"; - String[] args = new String[]{ id.serialize(), String.valueOf(insightsBannerTier) }; - - values.put(SEEN_INVITE_REMINDER, insightsBannerTier.id); - database.update(TABLE_NAME, values, query, args); - Recipient.live(id).refresh(); - } - - public void setExpireMessages(@NonNull RecipientId id, int expiration) { - ContentValues values = new ContentValues(1); - values.put(MESSAGE_EXPIRATION_TIME, expiration); - if (update(id, values)) { - Recipient.live(id).refresh(); - } - } - - public void setUnidentifiedAccessMode(@NonNull RecipientId id, @NonNull UnidentifiedAccessMode unidentifiedAccessMode) { - ContentValues values = new ContentValues(1); - values.put(UNIDENTIFIED_ACCESS_MODE, unidentifiedAccessMode.getMode()); - if (update(id, values)) { - Recipient.live(id).refresh(); - } - } - - public void setLastSessionResetTime(@NonNull RecipientId id, DeviceLastResetTime lastResetTime) { - ContentValues values = new ContentValues(1); - values.put(LAST_SESSION_RESET, lastResetTime.toByteArray()); - update(id, values); - } - - public @NonNull DeviceLastResetTime getLastSessionResetTimes(@NonNull RecipientId id) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - - try (Cursor cursor = db.query(TABLE_NAME, new String[] {LAST_SESSION_RESET}, ID_WHERE, SqlUtil.buildArgs(id), null, null, null)) { - if (cursor.moveToFirst()) { - try { - byte[] serialized = CursorUtil.requireBlob(cursor, LAST_SESSION_RESET); - if (serialized != null) { - return DeviceLastResetTime.parseFrom(serialized); - } else { - return DeviceLastResetTime.newBuilder().build(); - } - } catch (InvalidProtocolBufferException e) { - Log.w(TAG, e); - return DeviceLastResetTime.newBuilder().build(); - } - } - } - - return DeviceLastResetTime.newBuilder().build(); - } - - public void setBadges(@NonNull RecipientId id, @NonNull List badges) { - BadgeList.Builder badgeListBuilder = BadgeList.newBuilder(); - - for (final Badge badge : badges) { - badgeListBuilder.addBadges(Badges.toDatabaseBadge(badge)); - } - - ContentValues values = new ContentValues(1); - values.put(BADGES, badgeListBuilder.build().toByteArray()); - - if (update(id, values)) { - Recipient.live(id).refresh(); - } - } - - public void setCapabilities(@NonNull RecipientId id, @NonNull SignalServiceProfile.Capabilities capabilities) { - long value = 0; - - value = Bitmask.update(value, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv2()).serialize()); - value = Bitmask.update(value, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv1Migration()).serialize()); - value = Bitmask.update(value, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isSenderKey()).serialize()); - value = Bitmask.update(value, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isAnnouncementGroup()).serialize()); - value = Bitmask.update(value, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isChangeNumber()).serialize()); - - ContentValues values = new ContentValues(1); - values.put(CAPABILITIES, value); - - if (update(id, values)) { - Recipient.live(id).refresh(); - } - } - - public void setMentionSetting(@NonNull RecipientId id, @NonNull MentionSetting mentionSetting) { - ContentValues values = new ContentValues(); - values.put(MENTION_SETTING, mentionSetting.getId()); - if (update(id, values)) { - Recipient.live(id).refresh(); - } - } - - /** - * Updates the profile key. - *

- * If it changes, it clears out the profile key credential and resets the unidentified access mode. - * @return true iff changed. - */ - public boolean setProfileKey(@NonNull RecipientId id, @NonNull ProfileKey profileKey) { - String selection = ID + " = ?"; - String[] args = new String[]{id.serialize()}; - ContentValues valuesToCompare = new ContentValues(1); - ContentValues valuesToSet = new ContentValues(3); - String encodedProfileKey = Base64.encodeBytes(profileKey.serialize()); - - valuesToCompare.put(PROFILE_KEY, encodedProfileKey); - - valuesToSet.put(PROFILE_KEY, encodedProfileKey); - valuesToSet.putNull(PROFILE_KEY_CREDENTIAL); - valuesToSet.put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.getMode()); - - SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare); - - if (update(updateQuery, valuesToSet)) { - rotateStorageId(id); - Recipient.live(id).refresh(); - StorageSyncHelper.scheduleSyncForDataChange(); - return true; - } - return false; - } - - /** - * Sets the profile key iff currently null. - *

- * If it sets it, it also clears out the profile key credential and resets the unidentified access mode. - * @return true iff changed. - */ - public boolean setProfileKeyIfAbsent(@NonNull RecipientId id, @NonNull ProfileKey profileKey) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - String selection = ID + " = ? AND " + PROFILE_KEY + " is NULL"; - String[] args = new String[]{id.serialize()}; - ContentValues valuesToSet = new ContentValues(3); - - valuesToSet.put(PROFILE_KEY, Base64.encodeBytes(profileKey.serialize())); - valuesToSet.putNull(PROFILE_KEY_CREDENTIAL); - valuesToSet.put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.getMode()); - - if (database.update(TABLE_NAME, valuesToSet, selection, args) > 0) { - rotateStorageId(id); - Recipient.live(id).refresh(); - return true; - } else { - return false; - } - } - - /** - * Updates the profile key credential as long as the profile key matches. - */ - public boolean setProfileKeyCredential(@NonNull RecipientId id, - @NonNull ProfileKey profileKey, - @NonNull ProfileKeyCredential profileKeyCredential) - { - String selection = ID + " = ? AND " + PROFILE_KEY + " = ?"; - String[] args = new String[]{id.serialize(), Base64.encodeBytes(profileKey.serialize())}; - ContentValues values = new ContentValues(1); - - ProfileKeyCredentialColumnData columnData = ProfileKeyCredentialColumnData.newBuilder() - .setProfileKey(ByteString.copyFrom(profileKey.serialize())) - .setProfileKeyCredential(ByteString.copyFrom(profileKeyCredential.serialize())) - .build(); - - values.put(PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(columnData.toByteArray())); - - SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values); - - boolean updated = update(updateQuery, values); - - if (updated) { - Recipient.live(id).refresh(); - } - - return updated; - } - - private void clearProfileKeyCredential(@NonNull RecipientId id) { - ContentValues values = new ContentValues(1); - values.putNull(PROFILE_KEY_CREDENTIAL); - if (update(id, values)) { - rotateStorageId(id); - Recipient.live(id).refresh(); - } - } - - /** - * Fills in gaps (nulls) in profile key knowledge from new profile keys. - *

- * If from authoritative source, this will overwrite local, otherwise it will only write to the - * database if missing. - */ - public Set persistProfileKeySet(@NonNull ProfileKeySet profileKeySet) { - Map profileKeys = profileKeySet.getProfileKeys(); - Map authoritativeProfileKeys = profileKeySet.getAuthoritativeProfileKeys(); - int totalKeys = profileKeys.size() + authoritativeProfileKeys.size(); - - if (totalKeys == 0) { - return Collections.emptySet(); - } - - Log.i(TAG, String.format(Locale.US, "Persisting %d Profile keys, %d of which are authoritative", totalKeys, authoritativeProfileKeys.size())); - - HashSet updated = new HashSet<>(totalKeys); - RecipientId selfId = Recipient.self().getId(); - - for (Map.Entry entry : profileKeys.entrySet()) { - RecipientId recipientId = getOrInsertFromAci(entry.getKey()); - - if (setProfileKeyIfAbsent(recipientId, entry.getValue())) { - Log.i(TAG, "Learned new profile key"); - updated.add(recipientId); - } - } - - for (Map.Entry entry : authoritativeProfileKeys.entrySet()) { - RecipientId recipientId = getOrInsertFromAci(entry.getKey()); - - if (selfId.equals(recipientId)) { - Log.i(TAG, "Seen authoritative update for self"); - if (!entry.getValue().equals(ProfileKeyUtil.getSelfProfileKey())) { - Log.w(TAG, "Seen authoritative update for self that didn't match local, scheduling storage sync"); - StorageSyncHelper.scheduleSyncForDataChange(); - } - } else { - Log.i(TAG, String.format("Profile key from owner %s", recipientId)); - if (setProfileKey(recipientId, entry.getValue())) { - Log.i(TAG, "Learned new profile key from owner"); - updated.add(recipientId); - } - } - } - - return updated; - } - - public @NonNull List getSimilarRecipientIds(@NonNull Recipient recipient) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String[] projection = SqlUtil.buildArgs(ID, "COALESCE(" + nullIfEmpty(SYSTEM_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ") AS checked_name"); - String where = "checked_name = ?"; - - String[] arguments = SqlUtil.buildArgs(recipient.getProfileName().toString()); - - try (Cursor cursor = db.query(TABLE_NAME, projection, where, arguments, null, null, null)) { - if (cursor == null || cursor.getCount() == 0) { - return Collections.emptyList(); - } - - List results = new ArrayList<>(cursor.getCount()); - while (cursor.moveToNext()) { - results.add(RecipientId.from(CursorUtil.requireLong(cursor, ID))); - } - - return results; - } - } - - public void setProfileName(@NonNull RecipientId id, @NonNull ProfileName profileName) { - ContentValues contentValues = new ContentValues(1); - contentValues.put(PROFILE_GIVEN_NAME, profileName.getGivenName()); - contentValues.put(PROFILE_FAMILY_NAME, profileName.getFamilyName()); - contentValues.put(PROFILE_JOINED_NAME, profileName.toString()); - if (update(id, contentValues)) { - rotateStorageId(id); - Recipient.live(id).refresh(); - StorageSyncHelper.scheduleSyncForDataChange(); - } - } - - public void setProfileAvatar(@NonNull RecipientId id, @Nullable String profileAvatar) { - ContentValues contentValues = new ContentValues(1); - contentValues.put(SIGNAL_PROFILE_AVATAR, profileAvatar); - if (update(id, contentValues)) { - Recipient.live(id).refresh(); - - if (id.equals(Recipient.self().getId())) { - rotateStorageId(id); - StorageSyncHelper.scheduleSyncForDataChange(); - } - } - } - - public void setAbout(@NonNull RecipientId id, @Nullable String about, @Nullable String emoji) { - ContentValues contentValues = new ContentValues(); - contentValues.put(ABOUT, about); - contentValues.put(ABOUT_EMOJI, emoji); - - if (update(id, contentValues)) { - Recipient.live(id).refresh(); - } - } - - public void setProfileSharing(@NonNull RecipientId id, @SuppressWarnings("SameParameterValue") boolean enabled) { - ContentValues contentValues = new ContentValues(1); - contentValues.put(PROFILE_SHARING, enabled ? 1 : 0); - - boolean profiledUpdated = update(id, contentValues); - - if (profiledUpdated && enabled) { - Optional group = SignalDatabase.groups().getGroup(id); - - if (group.isPresent()) { - setHasGroupsInCommon(group.get().getMembers()); - } - } - - if (profiledUpdated) { - rotateStorageId(id); - Recipient.live(id).refresh(); - StorageSyncHelper.scheduleSyncForDataChange(); - } - } - - public void setNotificationChannel(@NonNull RecipientId id, @Nullable String notificationChannel) { - ContentValues contentValues = new ContentValues(1); - contentValues.put(NOTIFICATION_CHANNEL, notificationChannel); - if (update(id, contentValues)) { - Recipient.live(id).refresh(); - } - } - - public void resetAllWallpaper() { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - String[] selection = SqlUtil.buildArgs(ID, WALLPAPER_URI); - String where = WALLPAPER + " IS NOT NULL"; - List> idWithWallpaper = new LinkedList<>(); - - database.beginTransaction(); - - try { - try (Cursor cursor = database.query(TABLE_NAME, selection, where, null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - idWithWallpaper.add(new Pair<>(RecipientId.from(CursorUtil.requireInt(cursor, ID)), - CursorUtil.getString(cursor, WALLPAPER_URI).orNull())); - } - } - - if (idWithWallpaper.isEmpty()) { - return; - } - - ContentValues values = new ContentValues(2); - values.put(WALLPAPER_URI, (String) null); - values.put(WALLPAPER, (byte[]) null); - - int rowsUpdated = database.update(TABLE_NAME, values, where, null); - if (rowsUpdated == idWithWallpaper.size()) { - for (Pair pair : idWithWallpaper) { - Recipient.live(pair.first()).refresh(); - if (pair.second() != null) { - WallpaperStorage.onWallpaperDeselected(context, Uri.parse(pair.second())); - } - } - } else { - throw new AssertionError("expected " + idWithWallpaper.size() + " but got " + rowsUpdated); - } - - } finally { - database.setTransactionSuccessful(); - database.endTransaction(); - } - - } - - public void setWallpaper(@NonNull RecipientId id, @Nullable ChatWallpaper chatWallpaper) { - setWallpaper(id, chatWallpaper != null ? chatWallpaper.serialize() : null); - } - - private void setWallpaper(@NonNull RecipientId id, @Nullable Wallpaper wallpaper) { - Uri existingWallpaperUri = getWallpaperUri(id); - - ContentValues values = new ContentValues(); - values.put(WALLPAPER, wallpaper != null ? wallpaper.toByteArray() : null); - - if (wallpaper != null && wallpaper.hasFile()) { - values.put(WALLPAPER_URI, wallpaper.getFile().getUri()); - } else { - values.putNull(WALLPAPER_URI); - } - - if (update(id, values)) { - Recipient.live(id).refresh(); - } - - if (existingWallpaperUri != null) { - WallpaperStorage.onWallpaperDeselected(context, existingWallpaperUri); - } - } - - public void setDimWallpaperInDarkTheme(@NonNull RecipientId id, boolean enabled) { - Wallpaper wallpaper = getWallpaper(id); - - if (wallpaper == null) { - throw new IllegalStateException("No wallpaper set for " + id); - } - - Wallpaper updated = wallpaper.toBuilder() - .setDimLevelInDarkTheme(enabled ? ChatWallpaper.FIXED_DIM_LEVEL_FOR_DARK_THEME : 0) - .build(); - - setWallpaper(id, updated); - } - - private @Nullable Wallpaper getWallpaper(@NonNull RecipientId id) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - - try (Cursor cursor = db.query(TABLE_NAME, new String[] {WALLPAPER}, ID_WHERE, SqlUtil.buildArgs(id), null, null, null)) { - if (cursor.moveToFirst()) { - byte[] raw = CursorUtil.requireBlob(cursor, WALLPAPER); - - if (raw != null) { - try { - return Wallpaper.parseFrom(raw); - } catch (InvalidProtocolBufferException e) { - return null; - } - } else { - return null; - } - } - } - - return null; - } - - private @Nullable Uri getWallpaperUri(@NonNull RecipientId id) { - Wallpaper wallpaper = getWallpaper(id); - - if (wallpaper != null && wallpaper.hasFile()) { - return Uri.parse(wallpaper.getFile().getUri()); - } else { - return null; - } - } - - public int getWallpaperUriUsageCount(@NonNull Uri uri) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String query = WALLPAPER_URI + " = ?"; - String[] args = SqlUtil.buildArgs(uri); - - try (Cursor cursor = db.query(TABLE_NAME, new String[] { "COUNT(*)" }, query, args, null, null, null)) { - if (cursor.moveToFirst()) { - return cursor.getInt(0); - } - } - - return 0; - } - - /** - * @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.getSignalWritableDatabase(); - 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.getAci(), 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)) { - rotateStorageId(id); - Recipient.live(id).refresh(); - StorageSyncHelper.scheduleSyncForDataChange(); - } - } - - public void updateSelfPhone(@NonNull String e164) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.beginTransaction(); - - try { - RecipientId id = Recipient.self().getId(); - RecipientId newId = getAndPossiblyMerge(Recipient.self().requireAci(), e164, true, true); - - if (id.equals(newId)) { - Log.i(TAG, "[updateSelfPhone] Phone updated for self"); - } else { - throw new AssertionError("[updateSelfPhone] Self recipient id changed when updating phone. old: " + id + " new: " + newId); - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - public void setUsername(@NonNull RecipientId id, @Nullable String username) { - if (username != null) { - Optional existingUsername = getByUsername(username); - - if (existingUsername.isPresent() && !id.equals(existingUsername.get())) { - Log.i(TAG, "Username was previously thought to be owned by " + existingUsername.get() + ". Clearing their username."); - setUsername(existingUsername.get(), null); - } - } - - ContentValues contentValues = new ContentValues(1); - contentValues.put(USERNAME, username); - if (update(id, contentValues)) { - Recipient.live(id).refresh(); - StorageSyncHelper.scheduleSyncForDataChange(); - } - } - - public void clearUsernameIfExists(@NonNull String username) { - Optional existingUsername = getByUsername(username); - - if (existingUsername.isPresent()) { - setUsername(existingUsername.get(), null); - } - } - - public Set getAllPhoneNumbers() { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - Set results = new HashSet<>(); - - try (Cursor cursor = db.query(TABLE_NAME, new String[] { PHONE }, null, null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - String number = cursor.getString(cursor.getColumnIndexOrThrow(PHONE)); - - if (!TextUtils.isEmpty(number)) { - results.add(number); - } - } - } - - return results; - } - - /** - * @return True if setting the UUID resulted in changed recipientId, otherwise false. - */ - public boolean markRegistered(@NonNull RecipientId id, @NonNull ACI aci) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.beginTransaction(); - - try { - markRegisteredOrThrow(id, aci); - 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(aci, 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 ACI aci) { - ContentValues contentValues = new ContentValues(2); - contentValues.put(REGISTERED, RegisteredState.REGISTERED.getId()); - contentValues.put(ACI_COLUMN, aci.toString().toLowerCase()); - - if (update(id, contentValues)) { - setStorageIdIfNotSet(id); - Recipient.live(id).refresh(); - } - } - - public void markUnregistered(@NonNull RecipientId id) { - ContentValues contentValues = new ContentValues(2); - contentValues.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId()); - contentValues.putNull(STORAGE_SERVICE_ID); - - if (update(id, contentValues)) { - Recipient.live(id).refresh(); - } - } - - public void bulkUpdatedRegisteredStatus(@NonNull Map registered, Collection unregistered) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.beginTransaction(); - - try { - for (Map.Entry entry : registered.entrySet()) { - RecipientId recipientId = entry.getKey(); - ACI aci = entry.getValue(); - - ContentValues values = new ContentValues(2); - values.put(REGISTERED, RegisteredState.REGISTERED.getId()); - - if (aci != null) { - values.put(ACI_COLUMN, aci.toString().toLowerCase()); - } - - try { - if (update(recipientId, values)) { - setStorageIdIfNotSet(recipientId); - } - } catch (SQLiteConstraintException e) { - Log.w(TAG, "[bulkUpdateRegisteredStatus] Hit a conflict when trying to update " + recipientId + ". Possibly merging."); - - RecipientSettings existing = getRecipientSettings(entry.getKey()); - RecipientId newId = getAndPossiblyMerge(aci, existing.getE164(), true); - Log.w(TAG, "[bulkUpdateRegisteredStatus] Merged into " + newId); - } - } - - for (RecipientId id : unregistered) { - ContentValues values = new ContentValues(2); - values.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId()); - values.putNull(STORAGE_SERVICE_ID); - - update(id, values); - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - /** - * Handles inserts the (e164, UUID) pairs, which could result in merges. Does not mark users as - * registered. - * - * @return A mapping of (RecipientId, UUID) - */ - public @NonNull Map bulkProcessCdsResult(@NonNull Map mapping) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - HashMap aciMap = new HashMap<>(); - - db.beginTransaction(); - try { - for (Map.Entry entry : mapping.entrySet()) { - String e164 = entry.getKey(); - ACI aci = entry.getValue(); - Optional aciEntry = aci != null ? getByAci(aci) : Optional.absent(); - - if (aciEntry.isPresent()) { - boolean idChanged = setPhoneNumber(aciEntry.get(), e164); - if (idChanged) { - aciEntry = getByAci(Objects.requireNonNull(aci)); - } - } - - RecipientId id = aciEntry.isPresent() ? aciEntry.get() : getOrInsertFromE164(e164); - - aciMap.put(id, aci); - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - return aciMap; - } - - public @NonNull List getUninvitedRecipientsForInsights() { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - List results = new LinkedList<>(); - final String[] args = new String[]{String.valueOf(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31))}; - - try (Cursor cursor = db.rawQuery(INSIGHTS_INVITEE_LIST, args)) { - while (cursor != null && cursor.moveToNext()) { - results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))); - } - } - - return results; - } - - public @NonNull List getRegistered() { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - List results = new LinkedList<>(); - - try (Cursor cursor = db.query(TABLE_NAME, ID_PROJECTION, REGISTERED + " = ?", new String[] {"1"}, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))); - } - } - - return results; - } - - public List getSystemContacts() { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - List results = new LinkedList<>(); - - try (Cursor cursor = db.query(TABLE_NAME, ID_PROJECTION, SYSTEM_JOINED_NAME + " IS NOT NULL AND " + SYSTEM_JOINED_NAME + " != \"\"", null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))); - } - } - - return results; - } - - /** - * We no longer automatically generate a chat color. This method is used only - * in the case of a legacy migration and otherwise should not be called. - */ - @Deprecated - public void updateSystemContactColors() { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - Map updates = new HashMap<>(); - - db.beginTransaction(); - try (Cursor cursor = db.query(TABLE_NAME, new String[] {ID, "color", CHAT_COLORS, CUSTOM_CHAT_COLORS_ID, SYSTEM_JOINED_NAME}, SYSTEM_JOINED_NAME + " IS NOT NULL AND " + SYSTEM_JOINED_NAME + " != \"\"", null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - long id = CursorUtil.requireLong(cursor, ID); - String serializedColor = CursorUtil.requireString(cursor, "color"); - long customChatColorsId = CursorUtil.requireLong(cursor, CUSTOM_CHAT_COLORS_ID); - byte[] serializedChatColors = CursorUtil.requireBlob(cursor, CHAT_COLORS); - - ChatColors chatColors; - if (serializedChatColors != null) { - try { - chatColors = ChatColors.forChatColor(ChatColors.Id.forLongValue(customChatColorsId), ChatColor.parseFrom(serializedChatColors)); - } catch (InvalidProtocolBufferException e) { - chatColors = null; - } - } else { - chatColors = null; - } - - if (chatColors != null) { - return; - } - - if (serializedColor != null) { - try { - chatColors = ChatColorsMapper.getChatColors(MaterialColor.fromSerialized(serializedColor)); - } catch (MaterialColor.UnknownColorException e) { - return; - } - } else { - return; - } - - ContentValues contentValues = new ContentValues(1); - contentValues.put(CHAT_COLORS, chatColors.serialize().toByteArray()); - contentValues.put(CUSTOM_CHAT_COLORS_ID, chatColors.getId().getLongValue()); - - db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] { String.valueOf(id) }); - - updates.put(RecipientId.from(id), chatColors); - } - } finally { - db.setTransactionSuccessful(); - db.endTransaction(); - - Stream.of(updates.entrySet()).forEach(entry -> Recipient.live(entry.getKey()).refresh()); - } - } - - public @Nullable Cursor getSignalContacts(boolean includeSelf) { - ContactSearchSelection searchSelection = new ContactSearchSelection.Builder().withRegistered(true) - .withGroups(false) - .excludeId(includeSelf ? null : Recipient.self().getId()) - .build(); - - String selection = searchSelection.getWhere(); - String[] args = searchSelection.getArgs(); - String orderBy = SORT_NAME + ", " + SYSTEM_JOINED_NAME + ", " + SEARCH_PROFILE_NAME + ", " + USERNAME + ", " + PHONE; - - return databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy); - } - - public @Nullable Cursor querySignalContacts(@NonNull String query, boolean includeSelf) { - query = buildCaseInsensitiveGlobPattern(query); - - ContactSearchSelection searchSelection = new ContactSearchSelection.Builder().withRegistered(true) - .withGroups(false) - .excludeId(includeSelf ? null : Recipient.self().getId()) - .withSearchQuery(query) - .build(); - - String selection = searchSelection.getWhere(); - String[] args = searchSelection.getArgs(); - - String orderBy = SORT_NAME + ", " + SYSTEM_JOINED_NAME + ", " + SEARCH_PROFILE_NAME + ", " + PHONE; - - return databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy); - } - - public @Nullable Cursor getNonSignalContacts() { - ContactSearchSelection searchSelection = new ContactSearchSelection.Builder().withNonRegistered(true) - .withGroups(false) - .build(); - - String selection = searchSelection.getWhere(); - String[] args = searchSelection.getArgs(); - String orderBy = SYSTEM_JOINED_NAME + ", " + PHONE; - - return databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy); - } - - public @Nullable Cursor queryNonSignalContacts(@NonNull String query) { - query = buildCaseInsensitiveGlobPattern(query); - - ContactSearchSelection searchSelection = new ContactSearchSelection.Builder().withNonRegistered(true) - .withGroups(false) - .withSearchQuery(query) - .build(); - - String selection = searchSelection.getWhere(); - String[] args = searchSelection.getArgs(); - String orderBy = SYSTEM_JOINED_NAME + ", " + PHONE; - - return databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy); - } - - public @Nullable Cursor getNonGroupContacts(boolean includeSelf) { - ContactSearchSelection searchSelection = new ContactSearchSelection.Builder().withRegistered(true) - .withNonRegistered(true) - .withGroups(false) - .excludeId(includeSelf ? null : Recipient.self().getId()) - .build(); - - String orderBy = orderByPreferringAlphaOverNumeric(SORT_NAME) + ", " + PHONE; - - return databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, searchSelection.where, searchSelection.args, null, null, orderBy); - } - - public @Nullable Cursor queryNonGroupContacts(@NonNull String query, boolean includeSelf) { - query = buildCaseInsensitiveGlobPattern(query); - - ContactSearchSelection searchSelection = new ContactSearchSelection.Builder().withRegistered(true) - .withNonRegistered(true) - .withGroups(false) - .excludeId(includeSelf ? null : Recipient.self().getId()) - .withSearchQuery(query) - .build(); - - String selection = searchSelection.getWhere(); - String[] args = searchSelection.getArgs(); - String orderBy = orderByPreferringAlphaOverNumeric(SORT_NAME) + ", " + PHONE; - - return databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy); - } - - public @Nullable Cursor queryAllContacts(@NonNull String query) { - query = buildCaseInsensitiveGlobPattern(query); - - String selection = BLOCKED + " = ? AND " + - "(" + - SORT_NAME + " GLOB ? OR " + - USERNAME + " GLOB ? OR " + - PHONE + " GLOB ? OR " + - EMAIL + " GLOB ?" + - ")"; - String[] args = SqlUtil.buildArgs("0", query, query, query, query); - - return databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null); - } - - public @NonNull List queryRecipientsForMentions(@NonNull String query) { - return queryRecipientsForMentions(query, null); - } - - public @NonNull List queryRecipientsForMentions(@NonNull String query, @Nullable List recipientIds) { - query = buildCaseInsensitiveGlobPattern(query); - - String ids = null; - if (Util.hasItems(recipientIds)) { - ids = TextUtils.join(",", Stream.of(recipientIds).map(RecipientId::serialize).toList()); - } - - String selection = BLOCKED + " = 0 AND " + - (ids != null ? ID + " IN (" + ids + ") AND " : "") + - SORT_NAME + " GLOB ?"; - - List recipients = new ArrayList<>(); - try (RecipientDatabase.RecipientReader reader = new RecipientReader(databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, MENTION_SEARCH_PROJECTION, selection, SqlUtil.buildArgs(query), null, null, SORT_NAME))) { - Recipient recipient; - while ((recipient = reader.getNext()) != null) { - recipients.add(recipient); - } - } - return recipients; - } - - /** - * Builds a case-insensitive GLOB pattern for fuzzy text queries. Works with all unicode - * characters. - * - * Ex: - * cat -> [cC][aA][tT] - */ - private static String buildCaseInsensitiveGlobPattern(@NonNull String query) { - if (TextUtils.isEmpty(query)) { - return "*"; - } - - StringBuilder pattern = new StringBuilder(); - - for (int i = 0, len = query.codePointCount(0, query.length()); i < len; i++) { - String point = StringUtil.codePointToString(query.codePointAt(i)); - - pattern.append("["); - pattern.append(point.toLowerCase()); - pattern.append(point.toUpperCase()); - pattern.append(getAccentuatedCharRegex(point.toLowerCase())); - pattern.append("]"); - } - - return "*" + pattern.toString() + "*"; - } - - private static @NonNull String getAccentuatedCharRegex(@NonNull String query) { - switch (query) { - case "a" : - return "À-Åà-åĀ-ąǍǎǞ-ǡǺ-ǻȀ-ȃȦȧȺɐ-ɒḀḁẚẠ-ặ"; - case "b" : - return "ßƀ-ƅɃɓḂ-ḇ"; - case "c" : - return "çÇĆ-čƆ-ƈȻȼɔḈḉ"; - case "d" : - return "ÐðĎ-đƉ-ƍȡɖɗḊ-ḓ"; - case "e" : - return "È-Ëè-ëĒ-ěƎ-ƐǝȄ-ȇȨȩɆɇɘ-ɞḔ-ḝẸ-ệ"; - case "f" : - return "ƑƒḞḟ"; - case "g" : - return "Ĝ-ģƓǤ-ǧǴǵḠḡ"; - case "h" : - return "Ĥ-ħƕǶȞȟḢ-ḫẖ"; - case "i" : - return "Ì-Ïì-ïĨ-ıƖƗǏǐȈ-ȋɨɪḬ-ḯỈ-ị"; - case "j" : - return "ĴĵǰȷɈɉɟ"; - case "k" : - return "Ķ-ĸƘƙǨǩḰ-ḵ"; - case "l" : - return "Ĺ-łƚȴȽɫ-ɭḶ-ḽ"; - case "m" : - return "Ɯɯ-ɱḾ-ṃ"; - case "n" : - return "ÑñŃ-ŋƝƞǸǹȠȵɲ-ɴṄ-ṋ"; - case "o" : - return "Ò-ÖØò-öøŌ-őƟ-ơǑǒǪ-ǭǾǿȌ-ȏȪ-ȱṌ-ṓỌ-ợ"; - case "p" : - return "ƤƥṔ-ṗ"; - case "q" : - return ""; - case "r" : - return "Ŕ-řƦȐ-ȓɌɍṘ-ṟ"; - case "s" : - return "Ś-šƧƨȘșȿṠ-ṩ"; - case "t" : - return "Ţ-ŧƫ-ƮȚțȾṪ-ṱẗ"; - case "u" : - return "Ù-Üù-üŨ-ųƯ-ƱǓ-ǜȔ-ȗɄṲ-ṻỤ-ự"; - case "v" : - return "ƲɅṼ-ṿ"; - case "w" : - return "ŴŵẀ-ẉẘ"; - case "x" : - return "Ẋ-ẍ"; - case "y" : - return "ÝýÿŶ-ŸƔƳƴȲȳɎɏẎẏỲ-ỹỾỿẙ"; - case "z" : - return "Ź-žƵƶɀẐ-ẕ"; - case "α": - return "\u0386\u0391\u03AC\u03B1\u1F00-\u1F0F\u1F70\u1F71\u1F80-\u1F8F\u1FB0-\u1FB4\u1FB6-\u1FBC"; - case "ε" : - return "\u0388\u0395\u03AD\u03B5\u1F10-\u1F15\u1F18-\u1F1D\u1F72\u1F73\u1FC8\u1FC9"; - case "η" : - return "\u0389\u0397\u03AE\u03B7\u1F20-\u1F2F\u1F74\u1F75\u1F90-\u1F9F\u1F20-\u1F2F\u1F74\u1F75\u1F90-\u1F9F\u1fc2\u1fc3\u1fc4\u1fc6\u1FC7\u1FCA\u1FCB\u1FCC"; - case "ι" : - return "\u038A\u0390\u0399\u03AA\u03AF\u03B9\u03CA\u1F30-\u1F3F\u1F76\u1F77\u1FD0-\u1FD3\u1FD6-\u1FDB"; - case "ο" : - return "\u038C\u039F\u03BF\u03CC\u1F40-\u1F45\u1F48-\u1F4D\u1F78\u1F79\u1FF8\u1FF9"; - case "σ" : - return "\u03A3\u03C2\u03C3"; - case "ς" : - return "\u03A3\u03C2\u03C3"; - case "υ" : - return "\u038E\u03A5\u03AB\u03C5\u03CB\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F\u1F7A\u1F7B\u1FE0-\u1FE3\u1FE6-\u1FEB"; - case "ω" : - return "\u038F\u03A9\u03C9\u03CE\u1F60-\u1F6F\u1F7C\u1F7D\u1FA0-\u1FAF\u1FF2-\u1FF4\u1FF6\u1FF7\u1FFA-\u1FFC"; - default : - return ""; - } - } - - public @NonNull List getRecipientsForMultiDeviceSync() { - String subquery = "SELECT " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " FROM " + ThreadDatabase.TABLE_NAME; - String selection = REGISTERED + " = ? AND " + - GROUP_ID + " IS NULL AND " + - ID + " != ? AND " + - "(" + SYSTEM_CONTACT_URI + " NOT NULL OR " + ID + " IN (" + subquery + "))"; - String[] args = new String[] { String.valueOf(RegisteredState.REGISTERED.getId()), Recipient.self().getId().serialize() }; - - List recipients = new ArrayList<>(); - - try (Cursor cursor = databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, ID_PROJECTION, selection, args, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - recipients.add(Recipient.resolved(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID))))); - } - } - - return recipients; - } - - /** - * @param lastInteractionThreshold Only include contacts that have been interacted with since this time. - * @param lastProfileFetchThreshold Only include contacts that haven't their profile fetched after this time. - * @param limit Only return at most this many contact. - */ - public List getRecipientsForRoutineProfileFetch(long lastInteractionThreshold, long lastProfileFetchThreshold, int limit) { - ThreadDatabase threadDatabase = SignalDatabase.threads(); - Set recipientsWithinInteractionThreshold = new LinkedHashSet<>(); - - try (ThreadDatabase.Reader reader = threadDatabase.readerFor(threadDatabase.getRecentPushConversationList(-1, false))) { - ThreadRecord record; - - while ((record = reader.getNext()) != null && record.getDate() > lastInteractionThreshold) { - Recipient recipient = Recipient.resolved(record.getRecipient().getId()); - - if (recipient.isGroup()) { - recipientsWithinInteractionThreshold.addAll(recipient.getParticipants()); - } else { - recipientsWithinInteractionThreshold.add(recipient); - } - } - } - - return Stream.of(recipientsWithinInteractionThreshold) - .filterNot(Recipient::isSelf) - .filter(r -> r.getLastProfileFetchTime() < lastProfileFetchThreshold) - .limit(limit) - .map(Recipient::getId) - .toList(); - } - - public void markProfilesFetched(@NonNull Collection ids, long time) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.beginTransaction(); - try { - ContentValues values = new ContentValues(1); - values.put(LAST_PROFILE_FETCH, time); - - for (RecipientId id : ids) { - db.update(TABLE_NAME, values, ID_WHERE, new String[] { id.serialize() }); - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - public void applyBlockedUpdate(@NonNull List blocked, List groupIds) { - List blockedE164 = Stream.of(blocked) - .filter(b -> b.getNumber().isPresent()) - .map(b -> b.getNumber().get()) - .toList(); - List blockedUuid = Stream.of(blocked) - .map(b -> b.getAci().toString().toLowerCase()) - .toList(); - - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - try { - ContentValues resetBlocked = new ContentValues(); - resetBlocked.put(BLOCKED, 0); - db.update(TABLE_NAME, resetBlocked, null, null); - - ContentValues setBlocked = new ContentValues(); - setBlocked.put(BLOCKED, 1); - setBlocked.put(PROFILE_SHARING, 0); - - for (String e164 : blockedE164) { - db.update(TABLE_NAME, setBlocked, PHONE + " = ?", new String[] { e164 }); - } - - for (String uuid : blockedUuid) { - db.update(TABLE_NAME, setBlocked, ACI_COLUMN + " = ?", new String[] { uuid }); - } - - List groupIdStrings = new ArrayList<>(groupIds.size()); - - for (byte[] raw : groupIds) { - try { - groupIdStrings.add(GroupId.v1(raw)); - } catch (BadGroupIdException e) { - Log.w(TAG, "[applyBlockedUpdate] Bad GV1 ID!"); - } - } - - for (GroupId.V1 groupId : groupIdStrings) { - db.update(TABLE_NAME, setBlocked, GROUP_ID + " = ?", new String[] { groupId.toString() }); - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - ApplicationDependencies.getRecipientCache().clear(); - } - - public void updateStorageId(@NonNull RecipientId recipientId, byte[] id) { - updateStorageIds(Collections.singletonMap(recipientId, id)); - } - - public void updateStorageIds(@NonNull Map ids) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.beginTransaction(); - - try { - for (Map.Entry entry : ids.entrySet()) { - ContentValues values = new ContentValues(); - values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(entry.getValue())); - db.update(TABLE_NAME, values, ID_WHERE, new String[] { entry.getKey().serialize() }); - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - for (RecipientId id : ids.keySet()) { - Recipient.live(id).refresh(); - } - } - - public void markPreMessageRequestRecipientsAsProfileSharingEnabled(long messageRequestEnableTime) { - String[] whereArgs = SqlUtil.buildArgs(messageRequestEnableTime, messageRequestEnableTime); - - String select = "SELECT r." + ID + " FROM " + TABLE_NAME + " AS r " - + "INNER JOIN " + ThreadDatabase.TABLE_NAME + " AS t ON t." + ThreadDatabase.RECIPIENT_ID + " = r." + ID + " WHERE " - + "r." + PROFILE_SHARING + " = 0 AND " - + "(" - + "EXISTS(SELECT 1 FROM " + SmsDatabase.TABLE_NAME + " WHERE " + SmsDatabase.THREAD_ID + " = t." + ThreadDatabase.ID + " AND " + SmsDatabase.DATE_RECEIVED + " < ?) " - + "OR " - + "EXISTS(SELECT 1 FROM " + MmsDatabase.TABLE_NAME + " WHERE " + MmsDatabase.THREAD_ID + " = t." + ThreadDatabase.ID + " AND " + MmsDatabase.DATE_RECEIVED + " < ?) " - + ")"; - - List idsToUpdate = new ArrayList<>(); - try (Cursor cursor = databaseHelper.getSignalReadableDatabase().rawQuery(select, whereArgs)) { - while (cursor.moveToNext()) { - idsToUpdate.add(CursorUtil.requireLong(cursor, ID)); - } - } - - if (Util.hasItems(idsToUpdate)) { - SqlUtil.Query query = SqlUtil.buildCollectionQuery(ID, idsToUpdate); - ContentValues values = new ContentValues(1); - values.put(PROFILE_SHARING, 1); - databaseHelper.getSignalWritableDatabase().update(TABLE_NAME, values, query.getWhere(), query.getWhereArgs()); - - for (long id : idsToUpdate) { - Recipient.live(RecipientId.from(id)).refresh(); - } - } - } - - public void setHasGroupsInCommon(@NonNull List recipientIds) { - if (recipientIds.isEmpty()) { - return; - } - - SqlUtil.Query query = SqlUtil.buildCollectionQuery(ID, recipientIds); - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - try (Cursor cursor = db.query(TABLE_NAME, - new String[]{ID}, - query.getWhere() + " AND " + GROUPS_IN_COMMON + " = 0", - query.getWhereArgs(), - null, - null, - null)) - { - List idsToUpdate = new ArrayList<>(cursor.getCount()); - while (cursor.moveToNext()) { - idsToUpdate.add(CursorUtil.requireLong(cursor, ID)); - } - - if (Util.hasItems(idsToUpdate)) { - query = SqlUtil.buildCollectionQuery(ID, idsToUpdate); - ContentValues values = new ContentValues(); - values.put(GROUPS_IN_COMMON, 1); - int count = db.update(TABLE_NAME, values, query.getWhere(), query.getWhereArgs()); - if (count > 0) { - for (long id : idsToUpdate) { - Recipient.live(RecipientId.from(id)).refresh(); - } - } - } - } - } - - public void manuallyShowAvatar(@NonNull RecipientId recipientId) { - updateExtras(recipientId, b -> b.setManuallyShownAvatar(true)); - } - - private void updateExtras(@NonNull RecipientId recipientId, @NonNull Function updater) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.beginTransaction(); - try { - try (Cursor cursor = db.query(TABLE_NAME, new String[]{ID, EXTRAS}, ID_WHERE, SqlUtil.buildArgs(recipientId), null, null, null)) { - if (cursor.moveToNext()) { - RecipientExtras state = getRecipientExtras(cursor); - RecipientExtras.Builder builder = state != null ? state.toBuilder() : RecipientExtras.newBuilder(); - byte[] updatedState = updater.apply(builder).build().toByteArray(); - ContentValues values = new ContentValues(1); - values.put(EXTRAS, updatedState); - db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(CursorUtil.requireLong(cursor, ID))); - } - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - Recipient.live(recipientId).refresh(); - } - - /** - * Does not trigger any recipient refreshes -- it is assumed the caller handles this. - * Will *not* give storageIds to those that shouldn't get them (e.g. MMS groups, unregistered - * users). - */ - void rotateStorageId(@NonNull RecipientId recipientId) { - ContentValues values = new ContentValues(1); - values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())); - - String query = ID + " = ? AND (" + GROUP_TYPE + " IN (?, ?) OR " + REGISTERED + " = ?)"; - String[] args = SqlUtil.buildArgs(recipientId, GroupType.SIGNAL_V1.getId(), GroupType.SIGNAL_V2.getId(), RegisteredState.REGISTERED.getId()); - - databaseHelper.getSignalWritableDatabase().update(TABLE_NAME, values, query, args); - } - - /** - * Does not trigger any recipient refreshes -- it is assumed the caller handles this. - */ - void setStorageIdIfNotSet(@NonNull RecipientId recipientId) { - ContentValues values = new ContentValues(1); - values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())); - - String query = ID + " = ? AND " + STORAGE_SERVICE_ID + " IS NULL"; - String[] args = SqlUtil.buildArgs(recipientId); - - databaseHelper.getSignalWritableDatabase().update(TABLE_NAME, values, query, args); - } - - /** - * Updates a group recipient with a new V2 group ID. Should only be done as a part of GV1->GV2 - * migration. - */ - void updateGroupId(@NonNull GroupId.V1 v1Id, @NonNull GroupId.V2 v2Id) { - ContentValues values = new ContentValues(); - values.put(GROUP_ID, v2Id.toString()); - values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId()); - - SqlUtil.Query query = SqlUtil.buildTrueUpdateQuery(GROUP_ID + " = ?", SqlUtil.buildArgs(v1Id), values); - - if (update(query, values)) { - RecipientId id = getByGroupId(v2Id).get(); - rotateStorageId(id); - Recipient.live(id).refresh(); - } - } - - /** - * Will update the database with the content values you specified. It will make an intelligent - * query such that this will only return true if a row was *actually* updated. - */ - private boolean update(@NonNull RecipientId id, @NonNull ContentValues contentValues) { - SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(id), contentValues); - - return update(updateQuery, contentValues); - } - - /** - * Will update the database with the {@param contentValues} you specified. - *

- * This will only return true if a row was *actually* updated with respect to the where clause of the {@param updateQuery}. - */ - private boolean update(@NonNull SqlUtil.Query updateQuery, @NonNull ContentValues contentValues) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - - return database.update(TABLE_NAME, contentValues, updateQuery.getWhere(), updateQuery.getWhereArgs()) > 0; - } - - private @NonNull - Optional getByColumn(@NonNull String column, String value) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - String query = column + " = ?"; - String[] args = new String[] { value }; - - try (Cursor cursor = db.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - return Optional.of(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))); - } else { - return Optional.absent(); - } - } - } - - private @NonNull GetOrInsertResult getOrInsertByColumn(@NonNull String column, String value) { - if (TextUtils.isEmpty(value)) { - throw new AssertionError(column + " cannot be empty."); - } - - Optional existing = getByColumn(column, value); - - if (existing.isPresent()) { - return new GetOrInsertResult(existing.get(), false); - } else { - ContentValues values = new ContentValues(); - values.put(column, value); - values.put(AVATAR_COLOR, AvatarColor.random().serialize()); - - long id = databaseHelper.getSignalWritableDatabase().insert(TABLE_NAME, null, values); - - if (id < 0) { - existing = getByColumn(column, value); - - if (existing.isPresent()) { - return new GetOrInsertResult(existing.get(), false); - } else { - throw new AssertionError("Failed to insert recipient!"); - } - } else { - return new GetOrInsertResult(RecipientId.from(id), true); - } - } - } - - /** - * 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.getSignalWritableDatabase(); - - RecipientSettings uuidSettings = getRecipientSettings(byUuid); - RecipientSettings e164Settings = getRecipientSettings(byE164); - - // Identities - ApplicationDependencies.getIdentityStore().delete(e164Settings.e164); - - // 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 = SignalDatabase.groups(); - for (GroupDatabase.GroupRecord group : groupDatabase.getGroupsContainingMember(byE164, false, true)) { - LinkedHashSet newMembers = new LinkedHashSet<>(group.getMembers()); - newMembers.remove(byE164); - newMembers.add(byUuid); - - ContentValues groupValues = new ContentValues(); - groupValues.put(GroupDatabase.MEMBERS, RecipientId.toSerializedList(newMembers)); - db.update(GroupDatabase.TABLE_NAME, groupValues, GroupDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(group.getRecipientId())); - - if (group.isV2Group()) { - groupDatabase.removeUnmigratedV1Members(group.getId().requireV2(), Collections.singletonList(byE164)); - } - } - - // Threads - ThreadDatabase.MergeResult threadMerge = SignalDatabase.threads().merge(byUuid, byE164); - - // SMS Messages - ContentValues smsValues = new ContentValues(); - smsValues.put(SmsDatabase.RECIPIENT_ID, byUuid.serialize()); - db.update(SmsDatabase.TABLE_NAME, smsValues, SmsDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164)); - - if (threadMerge.neededMerge) { - ContentValues values = new ContentValues(); - values.put(SmsDatabase.THREAD_ID, threadMerge.threadId); - db.update(SmsDatabase.TABLE_NAME, values, SmsDatabase.THREAD_ID + " = ?", SqlUtil.buildArgs(threadMerge.previousThreadId)); - } - - // MMS Messages - ContentValues mmsValues = new ContentValues(); - mmsValues.put(MmsDatabase.RECIPIENT_ID, byUuid.serialize()); - db.update(MmsDatabase.TABLE_NAME, mmsValues, MmsDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164)); - - if (threadMerge.neededMerge) { - ContentValues values = new ContentValues(); - values.put(MmsDatabase.THREAD_ID, threadMerge.threadId); - db.update(MmsDatabase.TABLE_NAME, values, MmsDatabase.THREAD_ID + " = ?", SqlUtil.buildArgs(threadMerge.previousThreadId)); - } - - // Sessions - SessionDatabase sessionDatabase = SignalDatabase.sessions(); - - boolean hasE164Session = sessionDatabase.getAllFor(e164Settings.e164).size() > 0; - boolean hasUuidSession = sessionDatabase.getAllFor(uuidSettings.aci.toString()).size() > 0; - - if (hasE164Session && hasUuidSession) { - Log.w(TAG, "Had a session for both users. Deleting the E164.", true); - sessionDatabase.deleteAllFor(e164Settings.e164); - } else if (hasE164Session && !hasUuidSession) { - Log.w(TAG, "Had a session for E164, but not UUID. Re-assigning to the UUID.", true); - ContentValues values = new ContentValues(); - values.put(SessionDatabase.ADDRESS, uuidSettings.aci.toString()); - db.update(SessionDatabase.TABLE_NAME, values, SessionDatabase.ADDRESS + " = ?", SqlUtil.buildArgs(e164Settings.e164)); - } else if (!hasE164Session && hasUuidSession) { - Log.w(TAG, "Had a session for UUID, but not E164. No action necessary.", true); - } else { - Log.w(TAG, "Had no sessions. No action necessary.", true); - } - - // MSL - SignalDatabase.messageLog().remapRecipient(byE164, byUuid); - - // Mentions - ContentValues mentionRecipientValues = new ContentValues(); - mentionRecipientValues.put(MentionDatabase.RECIPIENT_ID, byUuid.serialize()); - db.update(MentionDatabase.TABLE_NAME, mentionRecipientValues, MentionDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164)); - if (threadMerge.neededMerge) { - ContentValues mentionThreadValues = new ContentValues(); - mentionThreadValues.put(MentionDatabase.THREAD_ID, threadMerge.threadId); - db.update(MentionDatabase.TABLE_NAME, mentionThreadValues, MentionDatabase.THREAD_ID + " = ?", SqlUtil.buildArgs(threadMerge.previousThreadId)); - } - - SignalDatabase.threads().setLastScrolled(threadMerge.threadId, 0); - SignalDatabase.threads().update(threadMerge.threadId, false, false); - - // Reactions - SignalDatabase.reactions().remapRecipient(byE164, byUuid); - - // Recipient - Log.w(TAG, "Deleting recipient " + byE164, true); - db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164)); - RemappedRecords.getInstance().addRecipient(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(CHAT_COLORS, Optional.fromNullable(uuidSettings.getChatColors()).or(Optional.fromNullable(e164Settings.getChatColors())).transform(colors -> colors.serialize().toByteArray()).orNull()); - uuidValues.put(AVATAR_COLOR, uuidSettings.getAvatarColor().serialize()); - uuidValues.put(CUSTOM_CHAT_COLORS_ID, Optional.fromNullable(uuidSettings.getChatColors()).or(Optional.fromNullable(e164Settings.getChatColors())).transform(colors -> colors.getId().getLongValue()).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_GIVEN_NAME, e164Settings.getSystemProfileName().getGivenName()); - uuidValues.put(SYSTEM_FAMILY_NAME, e164Settings.getSystemProfileName().getFamilyName()); - uuidValues.put(SYSTEM_JOINED_NAME, e164Settings.getSystemProfileName().toString()); - 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(CAPABILITIES, Math.max(uuidSettings.getCapabilities(), e164Settings.getCapabilities())); - uuidValues.put(MENTION_SETTING, uuidSettings.getMentionSetting() != MentionSetting.ALWAYS_NOTIFY ? uuidSettings.getMentionSetting().getId() : e164Settings.getMentionSetting().getId()); - 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)); - - 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.putNull(PROFILE_KEY_CREDENTIAL); - 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.getSignalWritableDatabase().inTransaction()) { - throw new IllegalStateException("Must be in a transaction!"); - } - } - - public class BulkOperationsHandle { - - private final SQLiteDatabase database; - - private final Map pendingContactInfoMap = new HashMap<>(); - - BulkOperationsHandle(SQLiteDatabase database) { - this.database = database; - } - - public void setSystemContactInfo(@NonNull RecipientId id, - @NonNull ProfileName systemProfileName, - @Nullable String systemDisplayName, - @Nullable String photoUri, - @Nullable String systemPhoneLabel, - int systemPhoneType, - @Nullable String systemContactUri) - { - String joinedName = Util.firstNonNull(systemDisplayName, systemProfileName.toString()); - - ContentValues refreshQualifyingValues = new ContentValues(); - refreshQualifyingValues.put(SYSTEM_GIVEN_NAME, systemProfileName.getGivenName()); - refreshQualifyingValues.put(SYSTEM_FAMILY_NAME, systemProfileName.getFamilyName()); - refreshQualifyingValues.put(SYSTEM_JOINED_NAME, joinedName); - refreshQualifyingValues.put(SYSTEM_PHOTO_URI, photoUri); - refreshQualifyingValues.put(SYSTEM_PHONE_LABEL, systemPhoneLabel); - refreshQualifyingValues.put(SYSTEM_PHONE_TYPE, systemPhoneType); - refreshQualifyingValues.put(SYSTEM_CONTACT_URI, systemContactUri); - - boolean updatedValues = update(id, refreshQualifyingValues); - - if (updatedValues) { - pendingContactInfoMap.put(id, new PendingContactInfo(systemProfileName, photoUri, systemPhoneLabel, systemContactUri)); - } - - ContentValues otherValues = new ContentValues(); - otherValues.put(SYSTEM_INFO_PENDING, 0); - update(id, otherValues); - } - - public void finish() { - markAllRelevantEntriesDirty(); - clearSystemDataForPendingInfo(); - - database.setTransactionSuccessful(); - database.endTransaction(); - - Stream.of(pendingContactInfoMap.entrySet()).forEach(entry -> Recipient.live(entry.getKey()).refresh()); - } - - private void markAllRelevantEntriesDirty() { - String query = SYSTEM_INFO_PENDING + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL"; - String[] args = SqlUtil.buildArgs("1"); - - try (Cursor cursor = database.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null)) { - while (cursor.moveToNext()) { - RecipientId id = RecipientId.from(CursorUtil.requireString(cursor, ID)); - rotateStorageId(id); - } - } - } - - private void clearSystemDataForPendingInfo() { - String query = SYSTEM_INFO_PENDING + " = ?"; - String[] args = new String[] { "1" }; - - ContentValues values = new ContentValues(5); - - values.put(SYSTEM_INFO_PENDING, 0); - values.put(SYSTEM_GIVEN_NAME, (String) null); - values.put(SYSTEM_FAMILY_NAME, (String) null); - values.put(SYSTEM_JOINED_NAME, (String) null); - values.put(SYSTEM_PHOTO_URI, (String) null); - values.put(SYSTEM_PHONE_LABEL, (String) null); - values.put(SYSTEM_CONTACT_URI, (String) null); - - database.update(TABLE_NAME, values, query, args); - } - } - - private static @NonNull String nullIfEmpty(String column) { - return "NULLIF(" + column + ", '')"; - } - - /** - * By default, SQLite will prefer numbers over letters when sorting. e.g. (b, a, 1) is sorted as (1, a, b). - * This order by will using a GLOB pattern to instead sort it as (a, b, 1). - * - * @param column The name of the column to sort by - */ - private static @NonNull String orderByPreferringAlphaOverNumeric(@NonNull String column) { - return "CASE WHEN " + column + " GLOB '[0-9]*' THEN 1 ELSE 0 END, " + column; - } - - private static @NonNull String removeWhitespace(@NonNull String column) { - return "REPLACE(" + column + ", ' ', '')"; - } - - public interface ColorUpdater { - ChatColors update(@NonNull String name, @Nullable MaterialColor materialColor); - } - - public static class RecipientSettings { - private final RecipientId id; - private final ACI aci; - private final String username; - private final String e164; - private final String email; - private final GroupId groupId; - private final GroupType groupType; - private final boolean blocked; - private final long muteUntil; - private final VibrateState messageVibrateState; - private final VibrateState callVibrateState; - private final Uri messageRingtone; - private final Uri callRingtone; - private final int defaultSubscriptionId; - private final int expireMessages; - private final RegisteredState registered; - private final byte[] profileKey; - private final ProfileKeyCredential profileKeyCredential; - private final ProfileName systemProfileName; - private final String systemDisplayName; - private final String systemContactPhoto; - private final String systemPhoneLabel; - private final String systemContactUri; - private final ProfileName signalProfileName; - private final String signalProfileAvatar; - private final boolean hasProfileImage; - private final boolean profileSharing; - private final long lastProfileFetch; - private final String notificationChannel; - private final UnidentifiedAccessMode unidentifiedAccessMode; - private final boolean forceSmsSelection; - private final long capabilities; - private final Recipient.Capability groupsV2Capability; - private final Recipient.Capability groupsV1MigrationCapability; - private final Recipient.Capability senderKeyCapability; - private final Recipient.Capability announcementGroupCapability; - private final Recipient.Capability changeNumberCapability; - private final InsightsBannerTier insightsBannerTier; - private final byte[] storageId; - private final MentionSetting mentionSetting; - private final ChatWallpaper wallpaper; - private final ChatColors chatColors; - private final AvatarColor avatarColor; - private final String about; - private final String aboutEmoji; - private final SyncExtras syncExtras; - private final Recipient.Extras extras; - private final boolean hasGroupsInCommon; - private final List badges; - - RecipientSettings(@NonNull RecipientId id, - @Nullable ACI uuid, - @Nullable String username, - @Nullable String e164, - @Nullable String email, - @Nullable GroupId groupId, - @NonNull GroupType groupType, - boolean blocked, - long muteUntil, - @NonNull VibrateState messageVibrateState, - @NonNull VibrateState callVibrateState, - @Nullable Uri messageRingtone, - @Nullable Uri callRingtone, - int defaultSubscriptionId, - int expireMessages, - @NonNull RegisteredState registered, - @Nullable byte[] profileKey, - @Nullable ProfileKeyCredential profileKeyCredential, - @NonNull ProfileName systemProfileName, - @Nullable String systemDisplayName, - @Nullable String systemContactPhoto, - @Nullable String systemPhoneLabel, - @Nullable String systemContactUri, - @NonNull ProfileName signalProfileName, - @Nullable String signalProfileAvatar, - boolean hasProfileImage, - boolean profileSharing, - long lastProfileFetch, - @Nullable String notificationChannel, - @NonNull UnidentifiedAccessMode unidentifiedAccessMode, - boolean forceSmsSelection, - long capabilities, - @NonNull InsightsBannerTier insightsBannerTier, - @Nullable byte[] storageId, - @NonNull MentionSetting mentionSetting, - @Nullable ChatWallpaper wallpaper, - @Nullable ChatColors chatColors, - @NonNull AvatarColor avatarColor, - @Nullable String about, - @Nullable String aboutEmoji, - @NonNull SyncExtras syncExtras, - @Nullable Recipient.Extras extras, - boolean hasGroupsInCommon, - @NonNull List badges) - { - this.id = id; - this.aci = uuid; - this.username = username; - this.e164 = e164; - this.email = email; - this.groupId = groupId; - this.groupType = groupType; - this.blocked = blocked; - this.muteUntil = muteUntil; - this.messageVibrateState = messageVibrateState; - this.callVibrateState = callVibrateState; - this.messageRingtone = messageRingtone; - this.callRingtone = callRingtone; - this.defaultSubscriptionId = defaultSubscriptionId; - this.expireMessages = expireMessages; - this.registered = registered; - this.profileKey = profileKey; - this.profileKeyCredential = profileKeyCredential; - this.systemProfileName = systemProfileName; - this.systemDisplayName = systemDisplayName; - this.systemContactPhoto = systemContactPhoto; - this.systemPhoneLabel = systemPhoneLabel; - this.systemContactUri = systemContactUri; - this.signalProfileName = signalProfileName; - this.signalProfileAvatar = signalProfileAvatar; - this.hasProfileImage = hasProfileImage; - this.profileSharing = profileSharing; - this.lastProfileFetch = lastProfileFetch; - this.notificationChannel = notificationChannel; - this.unidentifiedAccessMode = unidentifiedAccessMode; - this.forceSmsSelection = forceSmsSelection; - this.capabilities = capabilities; - this.groupsV2Capability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH)); - this.groupsV1MigrationCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH)); - this.senderKeyCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH)); - this.announcementGroupCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH)); - this.changeNumberCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH)); - this.insightsBannerTier = insightsBannerTier; - this.storageId = storageId; - this.mentionSetting = mentionSetting; - this.wallpaper = wallpaper; - this.chatColors = chatColors; - this.avatarColor = avatarColor; - this.about = about; - this.aboutEmoji = aboutEmoji; - this.syncExtras = syncExtras; - this.extras = extras; - this.hasGroupsInCommon = hasGroupsInCommon; - this.badges = badges; - } - - public RecipientId getId() { - return id; - } - - public @Nullable ACI getAci() { - return aci; - } - - public @Nullable String getUsername() { - return username; - } - - public @Nullable String getE164() { - return e164; - } - - public @Nullable String getEmail() { - return email; - } - - public @Nullable GroupId getGroupId() { - return groupId; - } - - public @NonNull GroupType getGroupType() { - return groupType; - } - - public boolean isBlocked() { - return blocked; - } - - public long getMuteUntil() { - return muteUntil; - } - - public @NonNull VibrateState getMessageVibrateState() { - return messageVibrateState; - } - - public @NonNull VibrateState getCallVibrateState() { - return callVibrateState; - } - - public @Nullable Uri getMessageRingtone() { - return messageRingtone; - } - - public @Nullable Uri getCallRingtone() { - return callRingtone; - } - - public @NonNull InsightsBannerTier getInsightsBannerTier() { - return insightsBannerTier; - } - - public Optional getDefaultSubscriptionId() { - return defaultSubscriptionId != -1 ? Optional.of(defaultSubscriptionId) : Optional.absent(); - } - - public int getExpireMessages() { - return expireMessages; - } - - public RegisteredState getRegistered() { - return registered; - } - - public @Nullable byte[] getProfileKey() { - return profileKey; - } - - public @Nullable ProfileKeyCredential getProfileKeyCredential() { - return profileKeyCredential; - } - - public @NonNull ProfileName getSystemProfileName() { - return systemProfileName; - } - - public @NonNull String getSystemDisplayName() { - return systemDisplayName; - } - - public @Nullable String getSystemContactPhotoUri() { - return systemContactPhoto; - } - - public @Nullable String getSystemPhoneLabel() { - return systemPhoneLabel; - } - - public @Nullable String getSystemContactUri() { - return systemContactUri; - } - - public @NonNull ProfileName getProfileName() { - return signalProfileName; - } - - public @Nullable String getProfileAvatar() { - return signalProfileAvatar; - } - - public boolean hasProfileImage() { - return hasProfileImage; - } - - public boolean isProfileSharing() { - return profileSharing; - } - - public long getLastProfileFetch() { - return lastProfileFetch; - } - - public @Nullable String getNotificationChannel() { - return notificationChannel; - } - - public @NonNull UnidentifiedAccessMode getUnidentifiedAccessMode() { - return unidentifiedAccessMode; - } - - public boolean isForceSmsSelection() { - return forceSmsSelection; - } - - public @NonNull Recipient.Capability getGroupsV2Capability() { - return groupsV2Capability; - } - - public @NonNull Recipient.Capability getGroupsV1MigrationCapability() { - return groupsV1MigrationCapability; - } - - public @NonNull Recipient.Capability getSenderKeyCapability() { - return senderKeyCapability; - } - - public @NonNull Recipient.Capability getAnnouncementGroupCapability() { - return announcementGroupCapability; - } - - public @NonNull Recipient.Capability getChangeNumberCapability() { - return changeNumberCapability; - } - - public @Nullable byte[] getStorageId() { - return storageId; - } - - public @NonNull MentionSetting getMentionSetting() { - return mentionSetting; - } - - public @Nullable ChatWallpaper getWallpaper() { - return wallpaper; - } - - public @Nullable ChatColors getChatColors() { - return chatColors; - } - - public @NonNull AvatarColor getAvatarColor() { - return avatarColor; - } - - public @Nullable String getAbout() { - return about; - } - - public @Nullable String getAboutEmoji() { - return aboutEmoji; - } - - public @NonNull SyncExtras getSyncExtras() { - return syncExtras; - } - - public @Nullable Recipient.Extras getExtras() { - return extras; - } - - public boolean hasGroupsInCommon() { - return hasGroupsInCommon; - } - - public @NonNull List getBadges() { - return badges; - } - - long getCapabilities() { - return capabilities; - } - - /** - * A bundle of data that's only necessary when syncing to storage service, not for a - * {@link Recipient}. - */ - public static class SyncExtras { - private final byte[] storageProto; - private final GroupMasterKey groupMasterKey; - private final byte[] identityKey; - private final VerifiedStatus identityStatus; - private final boolean archived; - private final boolean forcedUnread; - - public SyncExtras(@Nullable byte[] storageProto, - @Nullable GroupMasterKey groupMasterKey, - @Nullable byte[] identityKey, - @NonNull VerifiedStatus identityStatus, - boolean archived, - boolean forcedUnread) - { - this.storageProto = storageProto; - this.groupMasterKey = groupMasterKey; - this.identityKey = identityKey; - this.identityStatus = identityStatus; - this.archived = archived; - this.forcedUnread = forcedUnread; - } - - public @Nullable byte[] getStorageProto() { - return storageProto; - } - - public @Nullable GroupMasterKey getGroupMasterKey() { - return groupMasterKey; - } - - public boolean isArchived() { - return archived; - } - - public @Nullable byte[] getIdentityKey() { - return identityKey; - } - - public @NonNull VerifiedStatus getIdentityStatus() { - return identityStatus; - } - - public boolean isForcedUnread() { - return forcedUnread; - } - } - } - - public static class RecipientReader implements Closeable { - - private final Cursor cursor; - - RecipientReader(Cursor cursor) { - this.cursor = cursor; - } - - public @NonNull Recipient getCurrent() { - RecipientId id = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID))); - return Recipient.resolved(id); - } - - public @Nullable Recipient getNext() { - if (cursor != null && !cursor.moveToNext()) { - return null; - } - - return getCurrent(); - } - - public int getCount() { - if (cursor != null) return cursor.getCount(); - else return 0; - } - - public void close() { - cursor.close(); - } - } - - 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 ProfileName profileName; - private final String photoUri; - private final String phoneLabel; - private final String contactUri; - - private PendingContactInfo(@NonNull ProfileName systemProfileName, String photoUri, String phoneLabel, String contactUri) { - this.profileName = systemProfileName; - this.photoUri = photoUri; - this.phoneLabel = phoneLabel; - this.contactUri = contactUri; - } - } - - public static class MissingRecipientException extends IllegalStateException { - public MissingRecipientException(@Nullable RecipientId id) { - super("Failed to find recipient with ID: " + id); - } - } - - private static class GetOrInsertResult { - final RecipientId recipientId; - final boolean neededInsert; - - private GetOrInsertResult(@NonNull RecipientId recipientId, boolean neededInsert) { - this.recipientId = recipientId; - this.neededInsert = neededInsert; - } - } - - @VisibleForTesting - static final class ContactSearchSelection { - - static final String FILTER_GROUPS = " AND " + GROUP_ID + " IS NULL"; - static final String FILTER_ID = " AND " + ID + " != ?"; - static final String FILTER_BLOCKED = " AND " + BLOCKED + " = ?"; - - static final String NON_SIGNAL_CONTACT = REGISTERED + " != ? AND " + - SYSTEM_CONTACT_URI + " NOT NULL AND " + - "(" + PHONE + " NOT NULL OR " + EMAIL + " NOT NULL)"; - - static final String QUERY_NON_SIGNAL_CONTACT = NON_SIGNAL_CONTACT + - " AND (" + - PHONE + " GLOB ? OR " + - EMAIL + " GLOB ? OR " + - SYSTEM_JOINED_NAME + " GLOB ?" + - ")"; - - static final String SIGNAL_CONTACT = REGISTERED + " = ? AND " + - "(" + nullIfEmpty(SYSTEM_JOINED_NAME) + " NOT NULL OR " + PROFILE_SHARING + " = ?) AND " + - "(" + SORT_NAME + " NOT NULL OR " + USERNAME + " NOT NULL)"; - - static final String QUERY_SIGNAL_CONTACT = SIGNAL_CONTACT + " AND (" + - PHONE + " GLOB ? OR " + - SORT_NAME + " GLOB ? OR " + - USERNAME + " GLOB ?" + - ")"; - - private final String where; - private final String[] args; - - private ContactSearchSelection(@NonNull String where, @NonNull String[] args) { - this.where = where; - this.args = args; - } - - String getWhere() { - return where; - } - - String[] getArgs() { - return args; - } - - @VisibleForTesting - static final class Builder { - - private boolean includeRegistered; - private boolean includeNonRegistered; - private RecipientId excludeId; - private boolean excludeGroups; - private String searchQuery; - - @NonNull Builder withRegistered(boolean includeRegistered) { - this.includeRegistered = includeRegistered; - return this; - } - - @NonNull Builder withNonRegistered(boolean includeNonRegistered) { - this.includeNonRegistered = includeNonRegistered; - return this; - } - - @NonNull Builder excludeId(@Nullable RecipientId recipientId) { - this.excludeId = recipientId; - return this; - } - - @NonNull Builder withGroups(boolean includeGroups) { - this.excludeGroups = !includeGroups; - return this; - } - - @NonNull Builder withSearchQuery(@NonNull String searchQuery) { - this.searchQuery = searchQuery; - return this; - } - - @NonNull ContactSearchSelection build() { - if (!includeRegistered && !includeNonRegistered) { - throw new IllegalStateException("Must include either registered or non-registered recipients in search"); - } - - StringBuilder stringBuilder = new StringBuilder("("); - List args = new LinkedList<>(); - - if (includeRegistered) { - stringBuilder.append("("); - - args.add(RegisteredState.REGISTERED.id); - args.add(1); - - if (Util.isEmpty(searchQuery)) { - stringBuilder.append(SIGNAL_CONTACT); - } else { - stringBuilder.append(QUERY_SIGNAL_CONTACT); - args.add(searchQuery); - args.add(searchQuery); - args.add(searchQuery); - } - - stringBuilder.append(")"); - } - - if (includeRegistered && includeNonRegistered) { - stringBuilder.append(" OR "); - } - - if (includeNonRegistered) { - stringBuilder.append("("); - args.add(RegisteredState.REGISTERED.id); - - if (Util.isEmpty(searchQuery)) { - stringBuilder.append(NON_SIGNAL_CONTACT); - } else { - stringBuilder.append(QUERY_NON_SIGNAL_CONTACT); - args.add(searchQuery); - args.add(searchQuery); - args.add(searchQuery); - } - - stringBuilder.append(")"); - } - - stringBuilder.append(")"); - stringBuilder.append(FILTER_BLOCKED); - args.add(0); - - if (excludeGroups) { - stringBuilder.append(FILTER_GROUPS); - } - - if (excludeId != null) { - stringBuilder.append(FILTER_ID); - args.add(excludeId.serialize()); - } - - return new ContactSearchSelection(stringBuilder.toString(), args.stream().map(Object::toString).toArray(String[]::new)); - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt new file mode 100644 index 0000000000..6dbff382aa --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -0,0 +1,3250 @@ +package org.thoughtcrime.securesms.database + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.text.TextUtils +import androidx.annotation.VisibleForTesting +import com.google.protobuf.ByteString +import com.google.protobuf.InvalidProtocolBufferException +import net.zetetic.database.sqlcipher.SQLiteConstraintException +import org.signal.core.util.logging.Log +import org.signal.storageservice.protos.groups.local.DecryptedGroup +import org.signal.zkgroup.InvalidInputException +import org.signal.zkgroup.profiles.ProfileKey +import org.signal.zkgroup.profiles.ProfileKeyCredential +import org.thoughtcrime.securesms.badges.Badges +import org.thoughtcrime.securesms.badges.Badges.toDatabaseBadge +import org.thoughtcrime.securesms.badges.models.Badge +import org.thoughtcrime.securesms.color.MaterialColor +import org.thoughtcrime.securesms.color.MaterialColor.UnknownColorException +import org.thoughtcrime.securesms.conversation.colors.AvatarColor +import org.thoughtcrime.securesms.conversation.colors.ChatColors +import org.thoughtcrime.securesms.conversation.colors.ChatColors.Companion.forChatColor +import org.thoughtcrime.securesms.conversation.colors.ChatColors.Id.Companion.forLongValue +import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper.getChatColors +import org.thoughtcrime.securesms.crypto.ProfileKeyUtil +import org.thoughtcrime.securesms.database.GroupDatabase.LegacyGroupInsertException +import org.thoughtcrime.securesms.database.GroupDatabase.MissedGroupMigrationInsertException +import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.identities +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messageLog +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.reactions +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.sessions +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads +import org.thoughtcrime.securesms.database.model.RecipientRecord +import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList +import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor +import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime +import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileKeyCredentialColumnData +import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras +import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.groups.BadGroupIdException +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.groups.GroupId.V1 +import org.thoughtcrime.securesms.groups.GroupId.V2 +import org.thoughtcrime.securesms.groups.v2.ProfileKeySet +import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor +import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob +import org.thoughtcrime.securesms.jobs.RefreshAttributesJob +import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob +import org.thoughtcrime.securesms.jobs.RetrieveProfileJob +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.profiles.AvatarHelper +import org.thoughtcrime.securesms.profiles.ProfileName +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.storage.StorageRecordUpdate +import org.thoughtcrime.securesms.storage.StorageSyncHelper +import org.thoughtcrime.securesms.storage.StorageSyncModels +import org.thoughtcrime.securesms.util.Base64 +import org.thoughtcrime.securesms.util.Bitmask +import org.thoughtcrime.securesms.util.GroupUtil +import org.thoughtcrime.securesms.util.IdentityUtil +import org.thoughtcrime.securesms.util.SqlUtil +import org.thoughtcrime.securesms.util.StringUtil +import org.thoughtcrime.securesms.util.Util +import org.thoughtcrime.securesms.wallpaper.ChatWallpaper +import org.thoughtcrime.securesms.wallpaper.ChatWallpaperFactory +import org.thoughtcrime.securesms.wallpaper.WallpaperStorage +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.ACI +import org.whispersystems.signalservice.api.push.SignalServiceAddress +import org.whispersystems.signalservice.api.storage.SignalAccountRecord +import org.whispersystems.signalservice.api.storage.SignalContactRecord +import org.whispersystems.signalservice.api.storage.SignalGroupV1Record +import org.whispersystems.signalservice.api.storage.SignalGroupV2Record +import org.whispersystems.signalservice.api.storage.StorageId +import java.io.Closeable +import java.io.IOException +import java.lang.AssertionError +import java.lang.IllegalStateException +import java.lang.StringBuilder +import java.util.ArrayList +import java.util.Arrays +import java.util.Collections +import java.util.HashMap +import java.util.HashSet +import java.util.LinkedHashSet +import java.util.LinkedList +import java.util.Objects +import java.util.concurrent.TimeUnit +import kotlin.math.max + +open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : Database(context, databaseHelper) { + + companion object { + private val TAG = Log.tag(RecipientDatabase::class.java) + + const val TABLE_NAME = "recipient" + + const val ID = "_id" + private const val ACI_COLUMN = "uuid" + private const val USERNAME = "username" + const val PHONE = "phone" + const val EMAIL = "email" + const val GROUP_ID = "group_id" + const val GROUP_TYPE = "group_type" + private const val BLOCKED = "blocked" + private const val MESSAGE_RINGTONE = "message_ringtone" + private const val MESSAGE_VIBRATE = "message_vibrate" + private const val CALL_RINGTONE = "call_ringtone" + private const val CALL_VIBRATE = "call_vibrate" + private const val NOTIFICATION_CHANNEL = "notification_channel" + private const val MUTE_UNTIL = "mute_until" + private const val AVATAR_COLOR = "color" + private const val SEEN_INVITE_REMINDER = "seen_invite_reminder" + private const val DEFAULT_SUBSCRIPTION_ID = "default_subscription_id" + private const val MESSAGE_EXPIRATION_TIME = "message_expiration_time" + const val REGISTERED = "registered" + const val SYSTEM_JOINED_NAME = "system_display_name" + const val SYSTEM_FAMILY_NAME = "system_family_name" + const val SYSTEM_GIVEN_NAME = "system_given_name" + private const val SYSTEM_PHOTO_URI = "system_photo_uri" + const val SYSTEM_PHONE_TYPE = "system_phone_type" + const val SYSTEM_PHONE_LABEL = "system_phone_label" + private const val SYSTEM_CONTACT_URI = "system_contact_uri" + private const val SYSTEM_INFO_PENDING = "system_info_pending" + private const val PROFILE_KEY = "profile_key" + private const val PROFILE_KEY_CREDENTIAL = "profile_key_credential" + private const val SIGNAL_PROFILE_AVATAR = "signal_profile_avatar" + private const val PROFILE_SHARING = "profile_sharing" + private const val LAST_PROFILE_FETCH = "last_profile_fetch" + private const val UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode" + const val FORCE_SMS_SELECTION = "force_sms_selection" + private const val CAPABILITIES = "capabilities" + private const val STORAGE_SERVICE_ID = "storage_service_key" + private const val PROFILE_GIVEN_NAME = "signal_profile_name" + private const val PROFILE_FAMILY_NAME = "profile_family_name" + private const val PROFILE_JOINED_NAME = "profile_joined_name" + private const val MENTION_SETTING = "mention_setting" + private const val STORAGE_PROTO = "storage_proto" + private const val LAST_SESSION_RESET = "last_session_reset" + private const val WALLPAPER = "wallpaper" + private const val WALLPAPER_URI = "wallpaper_file" + const val ABOUT = "about" + const val ABOUT_EMOJI = "about_emoji" + private const val EXTRAS = "extras" + private const val GROUPS_IN_COMMON = "groups_in_common" + private const val CHAT_COLORS = "chat_colors" + private const val CUSTOM_CHAT_COLORS_ID = "custom_chat_colors_id" + private const val BADGES = "badges" + const val SEARCH_PROFILE_NAME = "search_signal_profile" + private const val SORT_NAME = "sort_name" + private const val IDENTITY_STATUS = "identity_status" + private const val IDENTITY_KEY = "identity_key" + + @JvmField + val CREATE_TABLE = + """ + CREATE TABLE $TABLE_NAME ( + $ID INTEGER PRIMARY KEY AUTOINCREMENT, + $ACI_COLUMN TEXT UNIQUE DEFAULT NULL, + $USERNAME TEXT UNIQUE DEFAULT NULL, + $PHONE TEXT UNIQUE DEFAULT NULL, + $EMAIL TEXT UNIQUE DEFAULT NULL, + $GROUP_ID TEXT UNIQUE DEFAULT NULL, + $GROUP_TYPE INTEGER DEFAULT ${GroupType.NONE.id}, + $BLOCKED INTEGER DEFAULT 0, + $MESSAGE_RINGTONE TEXT DEFAULT NULL, + $MESSAGE_VIBRATE INTEGER DEFAULT ${VibrateState.DEFAULT.id}, + $CALL_RINGTONE TEXT DEFAULT NULL, + $CALL_VIBRATE INTEGER DEFAULT ${VibrateState.DEFAULT.id}, + $NOTIFICATION_CHANNEL TEXT DEFAULT NULL, + $MUTE_UNTIL INTEGER DEFAULT 0, + $AVATAR_COLOR TEXT DEFAULT NULL, + $SEEN_INVITE_REMINDER INTEGER DEFAULT ${InsightsBannerTier.NO_TIER.id}, + $DEFAULT_SUBSCRIPTION_ID INTEGER DEFAULT -1, + $MESSAGE_EXPIRATION_TIME INTEGER DEFAULT 0, + $REGISTERED INTEGER DEFAULT ${RegisteredState.UNKNOWN.id}, + $SYSTEM_GIVEN_NAME TEXT DEFAULT NULL, + $SYSTEM_FAMILY_NAME TEXT DEFAULT NULL, + $SYSTEM_JOINED_NAME TEXT DEFAULT NULL, + $SYSTEM_PHOTO_URI TEXT DEFAULT NULL, + $SYSTEM_PHONE_LABEL TEXT DEFAULT NULL, + $SYSTEM_PHONE_TYPE INTEGER DEFAULT -1, + $SYSTEM_CONTACT_URI TEXT DEFAULT NULL, + $SYSTEM_INFO_PENDING INTEGER DEFAULT 0, + $PROFILE_KEY TEXT DEFAULT NULL, + $PROFILE_KEY_CREDENTIAL TEXT DEFAULT NULL, + $PROFILE_GIVEN_NAME TEXT DEFAULT NULL, + $PROFILE_FAMILY_NAME TEXT DEFAULT NULL, + $PROFILE_JOINED_NAME TEXT DEFAULT NULL, + $SIGNAL_PROFILE_AVATAR TEXT DEFAULT NULL, + $PROFILE_SHARING INTEGER DEFAULT 0, + $LAST_PROFILE_FETCH INTEGER DEFAULT 0, + $UNIDENTIFIED_ACCESS_MODE INTEGER DEFAULT 0, + $FORCE_SMS_SELECTION INTEGER DEFAULT 0, + $STORAGE_SERVICE_ID TEXT UNIQUE DEFAULT NULL, + $MENTION_SETTING INTEGER DEFAULT ${MentionSetting.ALWAYS_NOTIFY.id}, + $STORAGE_PROTO TEXT DEFAULT NULL, + $CAPABILITIES INTEGER DEFAULT 0, + $LAST_SESSION_RESET BLOB DEFAULT NULL, + $WALLPAPER BLOB DEFAULT NULL, + $WALLPAPER_URI TEXT DEFAULT NULL, + $ABOUT TEXT DEFAULT NULL, + $ABOUT_EMOJI TEXT DEFAULT NULL, + $EXTRAS BLOB DEFAULT NULL, + $GROUPS_IN_COMMON INTEGER DEFAULT 0, + $CHAT_COLORS BLOB DEFAULT NULL, + $CUSTOM_CHAT_COLORS_ID INTEGER DEFAULT 0, + $BADGES BLOB DEFAULT NULL + ) + """.trimIndent() + + val CREATE_INDEXS = arrayOf( + "CREATE INDEX IF NOT EXISTS recipient_group_type_index ON $TABLE_NAME ($GROUP_TYPE);" + ) + + private val RECIPIENT_PROJECTION: Array = arrayOf( + ID, + ACI_COLUMN, + USERNAME, + PHONE, + EMAIL, + GROUP_ID, + GROUP_TYPE, + BLOCKED, + MESSAGE_RINGTONE, + CALL_RINGTONE, + MESSAGE_VIBRATE, + CALL_VIBRATE, + MUTE_UNTIL, + AVATAR_COLOR, + SEEN_INVITE_REMINDER, + DEFAULT_SUBSCRIPTION_ID, + MESSAGE_EXPIRATION_TIME, + REGISTERED, + PROFILE_KEY, + PROFILE_KEY_CREDENTIAL, + SYSTEM_JOINED_NAME, + SYSTEM_GIVEN_NAME, + SYSTEM_FAMILY_NAME, + SYSTEM_PHOTO_URI, + SYSTEM_PHONE_LABEL, + SYSTEM_PHONE_TYPE, + SYSTEM_CONTACT_URI, + PROFILE_GIVEN_NAME, + PROFILE_FAMILY_NAME, + SIGNAL_PROFILE_AVATAR, + PROFILE_SHARING, + LAST_PROFILE_FETCH, + NOTIFICATION_CHANNEL, + UNIDENTIFIED_ACCESS_MODE, + FORCE_SMS_SELECTION, + CAPABILITIES, + STORAGE_SERVICE_ID, + MENTION_SETTING, + WALLPAPER, + WALLPAPER_URI, + MENTION_SETTING, + ABOUT, + ABOUT_EMOJI, + EXTRAS, + GROUPS_IN_COMMON, + CHAT_COLORS, + CUSTOM_CHAT_COLORS_ID, + BADGES + ) + + private val ID_PROJECTION = arrayOf(ID) + + private val SEARCH_PROJECTION = arrayOf( + ID, + SYSTEM_JOINED_NAME, + PHONE, + EMAIL, + SYSTEM_PHONE_LABEL, + SYSTEM_PHONE_TYPE, + REGISTERED, + ABOUT, + ABOUT_EMOJI, + EXTRAS, + GROUPS_IN_COMMON, + "COALESCE(NULLIF($PROFILE_JOINED_NAME, ''), NULLIF($PROFILE_GIVEN_NAME, '')) AS $SEARCH_PROFILE_NAME", + """ + LOWER( + COALESCE( + NULLIF($SYSTEM_JOINED_NAME, ''), + NULLIF($SYSTEM_GIVEN_NAME, ''), + NULLIF($PROFILE_JOINED_NAME, ''), + NULLIF($PROFILE_GIVEN_NAME, ''), + NULLIF($USERNAME, '') + ) + ) AS $SORT_NAME + """.trimIndent() + ) + + @JvmField + val SEARCH_PROJECTION_NAMES = arrayOf( + ID, + SYSTEM_JOINED_NAME, + PHONE, + EMAIL, + SYSTEM_PHONE_LABEL, + SYSTEM_PHONE_TYPE, + REGISTERED, + ABOUT, + ABOUT_EMOJI, + EXTRAS, + GROUPS_IN_COMMON, + SEARCH_PROFILE_NAME, + SORT_NAME + ) + + private val TYPED_RECIPIENT_PROJECTION: Array = RECIPIENT_PROJECTION + .map { columnName -> "$TABLE_NAME.$columnName" } + .toTypedArray() + + @JvmField + val TYPED_RECIPIENT_PROJECTION_NO_ID: Array = TYPED_RECIPIENT_PROJECTION.copyOfRange(1, TYPED_RECIPIENT_PROJECTION.size) + + private val MENTION_SEARCH_PROJECTION = arrayOf( + ID, + """ + REPLACE( + COALESCE( + NULLIF($SYSTEM_JOINED_NAME, ''), + NULLIF($SYSTEM_GIVEN_NAME, ''), + NULLIF($PROFILE_JOINED_NAME, ''), + NULLIF($PROFILE_GIVEN_NAME, ''), + NULLIF($USERNAME, ''), + NULLIF($PHONE, '') + ), + ' ', + '' + ) AS $SORT_NAME + """.trimIndent() + ) + + private val INSIGHTS_INVITEE_LIST = + """ + SELECT $TABLE_NAME.$ID + FROM $TABLE_NAME INNER JOIN ${ThreadDatabase.TABLE_NAME} ON $TABLE_NAME.$ID = ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.RECIPIENT_ID} + WHERE + $TABLE_NAME.$GROUP_ID IS NULL AND + $TABLE_NAME.$REGISTERED = ${RegisteredState.NOT_REGISTERED.id} AND + $TABLE_NAME.$SEEN_INVITE_REMINDER < ${InsightsBannerTier.TIER_TWO.id} AND + ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.HAS_SENT} AND + ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.DATE} > ? + ORDER BY ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.DATE} DESC LIMIT 50 + """ + } + + fun containsPhoneOrUuid(id: String): Boolean { + val query = "$ACI_COLUMN = ? OR $PHONE = ?" + val args = arrayOf(id, id) + readableDatabase.query(TABLE_NAME, arrayOf(ID), query, args, null, null, null).use { cursor -> return cursor != null && cursor.moveToFirst() } + } + + fun getByE164(e164: String): Optional { + return getByColumn(PHONE, e164) + } + + fun getByEmail(email: String): Optional { + return getByColumn(EMAIL, email) + } + + fun getByGroupId(groupId: GroupId): Optional { + return getByColumn(GROUP_ID, groupId.toString()) + } + + fun getByAci(uuid: ACI): Optional { + return getByColumn(ACI_COLUMN, uuid.toString()) + } + + fun getByUsername(username: String): Optional { + return getByColumn(USERNAME, username) + } + + fun getAndPossiblyMerge(aci: ACI?, e164: String?, highTrust: Boolean): RecipientId { + return getAndPossiblyMerge(aci, e164, highTrust, false) + } + + fun getAndPossiblyMerge(aci: ACI?, e164: String?, highTrust: Boolean, changeSelf: Boolean): RecipientId { + require(!(aci == null && e164 == null)) { "Must provide a UUID or E164!" } + + var recipientNeedingRefresh: RecipientId? = null + var remapped: Pair? = null + var recipientChangedNumber: RecipientId? = null + var transactionSuccessful = false + val db = writableDatabase + + db.beginTransaction() + try { + val byE164 = e164?.let { getByE164(it) } ?: Optional.absent() + val byAci = aci?.let { getByAci(it) } ?: Optional.absent() + val finalId: RecipientId + + if (!byE164.isPresent && !byAci.isPresent) { + Log.i(TAG, "Discovered a completely new user. Inserting.", true) + finalId = if (highTrust) { + val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(e164, aci)) + RecipientId.from(id) + } else { + val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(if (aci == null) e164 else null, aci)) + RecipientId.from(id) + } + } else if (byE164.isPresent && !byAci.isPresent) { + if (aci != null) { + val e164Record: RecipientRecord = getRecord(byE164.get()) + if (e164Record.aci != null) { + if (highTrust) { + Log.w(TAG, "Found out about an ACI ($aci) for a known E164 user (${byE164.get()}), but that user already has an ACI (${e164Record.aci}). Likely a case of re-registration. High-trust, so stripping the E164 from the existing account and assigning it to a new entry", true) + removePhoneNumber(byE164.get(), db) + recipientNeedingRefresh = byE164.get() + + val insertValues = buildContentValuesForNewUser(e164, aci) + insertValues.put(BLOCKED, if (e164Record.isBlocked) 1 else 0) + + val id = db.insert(TABLE_NAME, null, insertValues) + finalId = RecipientId.from(id) + } else { + Log.w(TAG, "Found out about an ACI ($aci) for a known E164 user (${byE164.get()}), but that user already has an ACI (${e164Record.aci}). Likely a case of re-registration. Low-trust, so making a new user for the UUID.", true) + val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(null, aci)) + finalId = RecipientId.from(id) + } + } else { + finalId = if (highTrust) { + Log.i(TAG, "Found out about an ACI ($aci) for a known E164 user (${byE164.get()}). High-trust, so updating.", true) + markRegisteredOrThrow(byE164.get(), aci) + byE164.get() + } else { + Log.i(TAG, "Found out about an ACI ($aci) for a known E164 user (${byE164.get()}). Low-trust, so making a new user for the ACI.", true) + val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(null, aci)) + RecipientId.from(id) + } + } + } else { + finalId = byE164.get() + } + } else if (!byE164.isPresent && byAci.isPresent) { + if (e164 != null) { + if (highTrust) { + if (aci == SignalStore.account().aci && !changeSelf) { + Log.w(TAG, "Found out about an E164 ($e164) for our own ACI user (${byAci.get()}). High-trust but not change self, doing nothing.", true) + finalId = byAci.get() + } else { + Log.i(TAG, "Found out about an E164 ($e164) for a known ACI user (${byAci.get()}). High-trust, so updating.", true) + val aciRecord: RecipientRecord = getRecord(byAci.get()) + setPhoneNumberOrThrow(byAci.get(), e164) + finalId = byAci.get() + + if (!Util.isEmpty(aciRecord.e164) && aciRecord.e164 != e164) { + recipientChangedNumber = finalId + } + } + } else { + Log.i(TAG, "Found out about an E164 (%s) for a known ACI user (%s). Low-trust, so doing nothing.", true) + finalId = byAci.get() + } + } else { + finalId = byAci.get() + } + } else { + if (byE164 == byAci) { + finalId = byAci.get() + } else { + Log.w(TAG, "Hit a conflict between ${byE164.get()} (E164 of $e164) and ${byAci.get()} (ACI $aci). They map to different recipients.", Throwable(), true) + val e164Record: RecipientRecord = getRecord(byE164.get()) + if (e164Record.aci != null) { + if (highTrust) { + Log.w(TAG, "The E164 contact has a different ACI. Likely a case of re-registration. High-trust, so stripping the E164 from the existing account and assigning it to the ACI entry.", true) + removePhoneNumber(byE164.get(), db) + + recipientNeedingRefresh = byE164.get() + val aciRecord: RecipientRecord = getRecord(byAci.get()) + setPhoneNumberOrThrow(byAci.get(), e164!!) + finalId = byAci.get() + + if (!Util.isEmpty(aciRecord.e164) && aciRecord.e164 != e164) { + recipientChangedNumber = finalId + } + } else { + Log.w(TAG, "The E164 contact has a different ACI. Likely a case of re-registration. Low-trust, so doing nothing.", true) + finalId = byAci.get() + } + } else { + val aciRecord: RecipientRecord = getRecord(byAci.get()) + if (aciRecord.e164 != null) { + if (highTrust) { + Log.w(TAG, "We have one contact with just an E164, and another with both an ACI and a different E164. High-trust, so merging the two rows together. The E164 has also effectively changed for the ACI contact.", true) + finalId = merge(byAci.get(), byE164.get()) + recipientNeedingRefresh = byAci.get() + remapped = Pair(byE164.get(), byAci.get()) + recipientChangedNumber = finalId + } else { + Log.w(TAG, "We have one contact with just an E164, and another with both an ACI and a different E164. Low-trust, so doing nothing.", true) + finalId = byAci.get() + } + } else { + if (highTrust) { + Log.w(TAG, "We have one contact with just an E164, and another with just an ACI. High-trust, so merging the two rows together.", true) + finalId = merge(byAci.get(), byE164.get()) + recipientNeedingRefresh = byAci.get() + remapped = Pair(byE164.get(), byAci.get()) + } else { + Log.w(TAG, "We have one contact with just an E164, and another with just an ACI. Low-trust, so doing nothing.", true) + finalId = byAci.get() + } + } + } + } + } + + db.setTransactionSuccessful() + transactionSuccessful = true + return finalId + } finally { + db.endTransaction() + + if (transactionSuccessful) { + if (recipientNeedingRefresh != null) { + Recipient.live(recipientNeedingRefresh).refresh() + RetrieveProfileJob.enqueue(recipientNeedingRefresh) + } + + if (remapped != null) { + Recipient.live(remapped.first()).refresh(remapped.second()) + ApplicationDependencies.getRecipientCache().remap(remapped.first(), remapped.second()) + } + + if (recipientNeedingRefresh != null || remapped != null) { + StorageSyncHelper.scheduleSyncForDataChange() + RecipientId.clearCache() + } + + if (recipientChangedNumber != null) { + ApplicationDependencies.getJobManager().add(RecipientChangedNumberJob(recipientChangedNumber)) + } + } + } + } + + fun getOrInsertFromAci(aci: ACI): RecipientId { + return getOrInsertByColumn(ACI_COLUMN, aci.toString()).recipientId + } + + fun getOrInsertFromE164(e164: String): RecipientId { + return getOrInsertByColumn(PHONE, e164).recipientId + } + + fun getOrInsertFromEmail(email: String): RecipientId { + return getOrInsertByColumn(EMAIL, email).recipientId + } + + fun getOrInsertFromGroupId(groupId: GroupId): RecipientId { + var existing = getByGroupId(groupId) + + if (existing.isPresent) { + return existing.get() + } else if (groupId.isV1 && groups.groupExists(groupId.requireV1().deriveV2MigrationGroupId())) { + throw LegacyGroupInsertException(groupId) + } else if (groupId.isV2 && groups.getGroupV1ByExpectedV2(groupId.requireV2()).isPresent) { + throw MissedGroupMigrationInsertException(groupId) + } else { + val values = ContentValues().apply { + put(GROUP_ID, groupId.toString()) + put(AVATAR_COLOR, AvatarColor.random().serialize()) + } + + val id = writableDatabase.insert(TABLE_NAME, null, values) + if (id < 0) { + existing = getByColumn(GROUP_ID, groupId.toString()) + if (existing.isPresent) { + return existing.get() + } else if (groupId.isV1 && groups.groupExists(groupId.requireV1().deriveV2MigrationGroupId())) { + throw LegacyGroupInsertException(groupId) + } else if (groupId.isV2 && groups.getGroupV1ByExpectedV2(groupId.requireV2()).isPresent) { + throw MissedGroupMigrationInsertException(groupId) + } else { + throw AssertionError("Failed to insert recipient!") + } + } else { + val groupUpdates = ContentValues().apply { + if (groupId.isMms) { + put(GROUP_TYPE, GroupType.MMS.id) + } else { + if (groupId.isV2) { + put(GROUP_TYPE, GroupType.SIGNAL_V2.id) + } else { + put(GROUP_TYPE, GroupType.SIGNAL_V1.id) + } + put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())) + } + } + + val recipientId = RecipientId.from(id) + update(recipientId, groupUpdates) + + return recipientId + } + } + } + + /** + * See [Recipient.externalPossiblyMigratedGroup]. + */ + fun getOrInsertFromPossiblyMigratedGroupId(groupId: GroupId): RecipientId { + val db = writableDatabase + db.beginTransaction() + + try { + val existing = getByColumn(GROUP_ID, groupId.toString()) + if (existing.isPresent) { + db.setTransactionSuccessful() + return existing.get() + } + + if (groupId.isV1) { + val v2 = getByGroupId(groupId.requireV1().deriveV2MigrationGroupId()) + if (v2.isPresent) { + db.setTransactionSuccessful() + return v2.get() + } + } + + if (groupId.isV2) { + val v1 = groups.getGroupV1ByExpectedV2(groupId.requireV2()) + if (v1.isPresent) { + db.setTransactionSuccessful() + return v1.get().recipientId + } + } + + val id = getOrInsertFromGroupId(groupId) + db.setTransactionSuccessful() + return id + } finally { + db.endTransaction() + } + } + + fun getBlocked(): Cursor { + return readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$BLOCKED = 1", null, null, null, null) + } + + fun readerForBlocked(cursor: Cursor): RecipientReader { + return RecipientReader(cursor) + } + + fun getRecipientsWithNotificationChannels(): RecipientReader { + val cursor = readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$NOTIFICATION_CHANNEL NOT NULL", null, null, null, null) + return RecipientReader(cursor) + } + + fun getRecord(id: RecipientId): RecipientRecord { + val query = "$ID = ?" + val args = arrayOf(id.serialize()) + + readableDatabase.query(TABLE_NAME, RECIPIENT_PROJECTION, query, args, null, null, null).use { cursor -> + return if (cursor != null && cursor.moveToNext()) { + getRecord(context, cursor) + } else { + val remapped = RemappedRecords.getInstance().getRecipient(id) + + if (remapped.isPresent) { + Log.w(TAG, "Missing recipient for $id, but found it in the remapped records as ${remapped.get()}") + getRecord(remapped.get()) + } else { + throw MissingRecipientException(id) + } + } + } + } + + fun getRecordForSync(id: RecipientId): RecipientRecord? { + val query = "$TABLE_NAME.$ID = ?" + val args = arrayOf(id.serialize()) + val recordForSync = getRecordForSync(query, args) + + if (recordForSync.isEmpty()) { + return null + } + + if (recordForSync.size > 1) { + throw AssertionError() + } + + return recordForSync[0] + } + + fun getByStorageId(storageId: ByteArray): RecipientRecord? { + val result = getRecordForSync("$TABLE_NAME.$STORAGE_SERVICE_ID = ?", arrayOf(Base64.encodeBytes(storageId))) + + return if (result.isNotEmpty()) { + result[0] + } else null + } + + fun markNeedsSyncWithoutRefresh(recipientIds: Collection) { + val db = writableDatabase + db.beginTransaction() + try { + for (recipientId in recipientIds) { + rotateStorageId(recipientId) + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + fun markNeedsSync(recipientId: RecipientId) { + rotateStorageId(recipientId) + Recipient.live(recipientId).refresh() + } + + fun applyStorageIdUpdates(storageIds: Map) { + val db = writableDatabase + db.beginTransaction() + try { + val query = "$ID = ?" + for ((key, value) in storageIds) { + val values = ContentValues().apply { + put(STORAGE_SERVICE_ID, Base64.encodeBytes(value.raw)) + } + db.update(TABLE_NAME, values, query, arrayOf(key.serialize())) + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + + for (id in storageIds.keys) { + Recipient.live(id).refresh() + } + } + + fun applyStorageSyncContactInsert(insert: SignalContactRecord) { + val db = writableDatabase + val threadDatabase = threads + val values = getValuesForStorageContact(insert, true) + val id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE) + + val recipientId: RecipientId? + if (id < 0) { + Log.w(TAG, "[applyStorageSyncContactInsert] Failed to insert. Possibly merging.") + recipientId = getAndPossiblyMerge(if (insert.address.hasValidAci()) insert.address.aci else null, insert.address.number.orNull(), true) + db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId)) + } else { + recipientId = RecipientId.from(id) + } + + if (insert.identityKey.isPresent && insert.address.hasValidAci()) { + try { + val identityKey = IdentityKey(insert.identityKey.get(), 0) + identities.updateIdentityAfterSync(insert.address.identifier, recipientId!!, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.identityState)) + } catch (e: InvalidKeyException) { + Log.w(TAG, "Failed to process identity key during insert! Skipping.", e) + } + } + + threadDatabase.applyStorageSyncUpdate(recipientId!!, insert) + } + + fun applyStorageSyncContactUpdate(update: StorageRecordUpdate) { + val db = writableDatabase + val identityStore = ApplicationDependencies.getIdentityStore() + val values = getValuesForStorageContact(update.new, false) + + try { + val updateCount = db.update(TABLE_NAME, values, "$STORAGE_SERVICE_ID = ?", arrayOf(Base64.encodeBytes(update.old.id.raw))) + if (updateCount < 1) { + throw AssertionError("Had an update, but it didn't match any rows!") + } + } catch (e: SQLiteConstraintException) { + Log.w(TAG, "[applyStorageSyncContactUpdate] Failed to update a user by storageId.") + var recipientId = getByColumn(STORAGE_SERVICE_ID, Base64.encodeBytes(update.old.id.raw)).get() + + Log.w(TAG, "[applyStorageSyncContactUpdate] Found user $recipientId. Possibly merging.") + recipientId = getAndPossiblyMerge(if (update.new.address.hasValidAci()) update.new.address.aci else null, update.new.address.number.orNull(), true) + + Log.w(TAG, "[applyStorageSyncContactUpdate] Merged into $recipientId") + db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId)) + } + + val recipientId = getByStorageKeyOrThrow(update.new.id.raw) + if (StorageSyncHelper.profileKeyChanged(update)) { + val clearValues = ContentValues(1).apply { + putNull(PROFILE_KEY_CREDENTIAL) + } + db.update(TABLE_NAME, clearValues, ID_WHERE, SqlUtil.buildArgs(recipientId)) + } + + try { + val oldIdentityRecord = identityStore.getIdentityRecord(recipientId) + if (update.new.identityKey.isPresent && update.new.address.hasValidAci()) { + val identityKey = IdentityKey(update.new.identityKey.get(), 0) + identities.updateIdentityAfterSync(update.new.address.identifier, recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(update.new.identityState)) + } + + val newIdentityRecord = identityStore.getIdentityRecord(recipientId) + if (newIdentityRecord.isPresent && newIdentityRecord.get().verifiedStatus == VerifiedStatus.VERIFIED && (!oldIdentityRecord.isPresent || oldIdentityRecord.get().verifiedStatus != VerifiedStatus.VERIFIED)) { + IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), true, true) + } else if (newIdentityRecord.isPresent && newIdentityRecord.get().verifiedStatus != VerifiedStatus.VERIFIED && oldIdentityRecord.isPresent && oldIdentityRecord.get().verifiedStatus == VerifiedStatus.VERIFIED) { + IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), false, true) + } + } catch (e: InvalidKeyException) { + Log.w(TAG, "Failed to process identity key during update! Skipping.", e) + } + + threads.applyStorageSyncUpdate(recipientId, update.new) + Recipient.live(recipientId).refresh() + } + + fun applyStorageSyncGroupV1Insert(insert: SignalGroupV1Record) { + val id = writableDatabase.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert, true)) + + val recipientId = RecipientId.from(id) + threads.applyStorageSyncUpdate(recipientId, insert) + Recipient.live(recipientId).refresh() + } + + fun applyStorageSyncGroupV1Update(update: StorageRecordUpdate) { + val values = getValuesForStorageGroupV1(update.new, false) + + val updateCount = writableDatabase.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", arrayOf(Base64.encodeBytes(update.old.id.raw))) + if (updateCount < 1) { + throw AssertionError("Had an update, but it didn't match any rows!") + } + + val recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(update.old.groupId)) + threads.applyStorageSyncUpdate(recipient.id, update.new) + recipient.live().refresh() + } + + fun applyStorageSyncGroupV2Insert(insert: SignalGroupV2Record) { + val masterKey = insert.masterKeyOrThrow + val groupId = GroupId.v2(masterKey) + val values = getValuesForStorageGroupV2(insert, true) + + writableDatabase.insertOrThrow(TABLE_NAME, null, values) + val recipient = Recipient.externalGroupExact(context, groupId) + + Log.i(TAG, "Creating restore placeholder for $groupId") + groups.create( + masterKey, + DecryptedGroup.newBuilder() + .setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) + .build() + ) + + Log.i(TAG, "Scheduling request for latest group info for $groupId") + ApplicationDependencies.getJobManager().add(RequestGroupV2InfoJob(groupId)) + threads.applyStorageSyncUpdate(recipient.id, insert) + recipient.live().refresh() + } + + fun applyStorageSyncGroupV2Update(update: StorageRecordUpdate) { + val values = getValuesForStorageGroupV2(update.new, false) + + val updateCount = writableDatabase.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", arrayOf(Base64.encodeBytes(update.old.id.raw))) + if (updateCount < 1) { + throw AssertionError("Had an update, but it didn't match any rows!") + } + + val masterKey = update.old.masterKeyOrThrow + val recipient = Recipient.externalGroupExact(context, GroupId.v2(masterKey)) + + threads.applyStorageSyncUpdate(recipient.id, update.new) + recipient.live().refresh() + } + + fun applyStorageSyncAccountUpdate(update: StorageRecordUpdate) { + val profileName = ProfileName.fromParts(update.new.givenName.orNull(), update.new.familyName.orNull()) + val localKey = ProfileKeyUtil.profileKeyOptional(update.old.profileKey.orNull()) + val remoteKey = ProfileKeyUtil.profileKeyOptional(update.new.profileKey.orNull()) + val profileKey = remoteKey.or(localKey).transform { obj: ProfileKey -> obj.serialize() }.transform { source: ByteArray? -> Base64.encodeBytes(source!!) }.orNull() + if (!remoteKey.isPresent) { + Log.w(TAG, "Got an empty profile key while applying an account record update!") + } + + val values = ContentValues().apply { + put(PROFILE_GIVEN_NAME, profileName.givenName) + put(PROFILE_FAMILY_NAME, profileName.familyName) + put(PROFILE_JOINED_NAME, profileName.toString()) + put(PROFILE_KEY, profileKey) + put(STORAGE_SERVICE_ID, Base64.encodeBytes(update.new.id.raw)) + if (update.new.hasUnknownFields()) { + put(STORAGE_PROTO, Base64.encodeBytes(Objects.requireNonNull(update.new.serializeUnknownFields()))) + } else { + putNull(STORAGE_PROTO) + } + } + + val updateCount = writableDatabase.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", arrayOf(Base64.encodeBytes(update.old.id.raw))) + if (updateCount < 1) { + throw AssertionError("Account update didn't match any rows!") + } + + if (remoteKey != localKey) { + ApplicationDependencies.getJobManager().add(RefreshAttributesJob()) + } + + threads.applyStorageSyncUpdate(Recipient.self().id, update.new) + Recipient.self().live().refresh() + } + + fun updatePhoneNumbers(mapping: Map) { + if (mapping.isEmpty()) return + val db = writableDatabase + + db.beginTransaction() + try { + val query = "$PHONE = ?" + for ((key, value) in mapping) { + val values = ContentValues().apply { + put(PHONE, value) + } + db.updateWithOnConflict(TABLE_NAME, values, query, arrayOf(key), SQLiteDatabase.CONFLICT_IGNORE) + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + private fun getByStorageKeyOrThrow(storageKey: ByteArray): RecipientId { + val query = "$STORAGE_SERVICE_ID = ?" + val args = arrayOf(Base64.encodeBytes(storageKey)) + + readableDatabase.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null).use { cursor -> + return if (cursor != null && cursor.moveToFirst()) { + val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)) + RecipientId.from(id) + } else { + throw AssertionError("No recipient with that storage key!") + } + } + } + + private fun getRecordForSync(query: String?, args: Array?): List { + val table = + """ + $TABLE_NAME LEFT OUTER JOIN ${IdentityDatabase.TABLE_NAME} ON $TABLE_NAME.$ACI_COLUMN = ${IdentityDatabase.TABLE_NAME}.${IdentityDatabase.ADDRESS} + LEFT OUTER JOIN ${GroupDatabase.TABLE_NAME} ON $TABLE_NAME.$GROUP_ID = ${GroupDatabase.TABLE_NAME}.${GroupDatabase.GROUP_ID} + LEFT OUTER JOIN ${ThreadDatabase.TABLE_NAME} ON $TABLE_NAME.$ID = ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.RECIPIENT_ID} + """.trimIndent() + val out: MutableList = ArrayList() + val columns: Array = TYPED_RECIPIENT_PROJECTION + arrayOf( + "$TABLE_NAME.$STORAGE_PROTO", + "${GroupDatabase.TABLE_NAME}.${GroupDatabase.V2_MASTER_KEY}", + "${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ARCHIVED}", + "${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.READ}", + "${IdentityDatabase.TABLE_NAME}.${IdentityDatabase.VERIFIED} AS $IDENTITY_STATUS", + "${IdentityDatabase.TABLE_NAME}.${IdentityDatabase.IDENTITY_KEY} AS $IDENTITY_KEY" + ) + + readableDatabase.query(table, columns, query, args, "$TABLE_NAME.$ID", null, null).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + out.add(getRecord(context, cursor)) + } + } + + return out + } + + /** + * @return All storage ids for ContactRecords, excluding the ones that need to be deleted. + */ + fun getContactStorageSyncIds(): List { + return ArrayList(getContactStorageSyncIdsMap().values) + } + + /** + * @return All storage IDs for ContactRecords, excluding the ones that need to be deleted. + */ + fun getContactStorageSyncIdsMap(): Map { + val query = "$STORAGE_SERVICE_ID NOT NULL AND $ACI_COLUMN NOT NULL AND $ID != ? AND $GROUP_TYPE != ?" + val args = SqlUtil.buildArgs(Recipient.self().id, GroupType.SIGNAL_V2.id.toString()) + val out: MutableMap = HashMap() + + readableDatabase.query(TABLE_NAME, arrayOf(ID, STORAGE_SERVICE_ID, GROUP_TYPE), query, args, null, null, null).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + val id = RecipientId.from(cursor.requireLong(ID)) + val encodedKey = cursor.requireNonNullString(STORAGE_SERVICE_ID) + val groupType = GroupType.fromId(cursor.requireInt(GROUP_TYPE)) + val key = Base64.decodeOrThrow(encodedKey) + + when (groupType) { + GroupType.NONE -> out[id] = StorageId.forContact(key) + GroupType.SIGNAL_V1 -> out[id] = StorageId.forGroupV1(key) + else -> throw AssertionError() + } + } + } + + for (id in groups.allGroupV2Ids) { + val recipient = Recipient.externalGroupExact(context, id!!) + val recipientId = recipient.id + val existing: RecipientRecord = getRecordForSync(recipientId) ?: throw AssertionError() + val key = existing.storageId ?: throw AssertionError() + out[recipientId] = StorageId.forGroupV2(key) + } + + return out + } + + fun beginBulkSystemContactUpdate(): BulkOperationsHandle { + val db = writableDatabase + val contentValues = ContentValues(1).apply { + put(SYSTEM_INFO_PENDING, 1) + } + + db.beginTransaction() + db.update(TABLE_NAME, contentValues, "$SYSTEM_CONTACT_URI NOT NULL", null) + return BulkOperationsHandle(db) + } + + fun onUpdatedChatColors(chatColors: ChatColors) { + val where = "$CUSTOM_CHAT_COLORS_ID = ?" + val args = SqlUtil.buildArgs(chatColors.id.longValue) + val updated: MutableList = LinkedList() + + readableDatabase.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + updated.add(RecipientId.from(cursor.requireLong(ID))) + } + } + + if (updated.isEmpty()) { + Log.d(TAG, "No recipients utilizing updated chat color.") + } else { + val values = ContentValues(2).apply { + put(CHAT_COLORS, chatColors.serialize().toByteArray()) + put(CUSTOM_CHAT_COLORS_ID, chatColors.id.longValue) + } + + writableDatabase.update(TABLE_NAME, values, where, args) + + for (recipientId in updated) { + Recipient.live(recipientId).refresh() + } + } + } + + fun onDeletedChatColors(chatColors: ChatColors) { + val where = "$CUSTOM_CHAT_COLORS_ID = ?" + val args = SqlUtil.buildArgs(chatColors.id.longValue) + val updated: MutableList = LinkedList() + + readableDatabase.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + updated.add(RecipientId.from(cursor.requireLong(ID))) + } + } + + if (updated.isEmpty()) { + Log.d(TAG, "No recipients utilizing deleted chat color.") + } else { + val values = ContentValues(2).apply { + put(CHAT_COLORS, null as ByteArray?) + put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.longValue) + } + + writableDatabase.update(TABLE_NAME, values, where, args) + + for (recipientId in updated) { + Recipient.live(recipientId).refresh() + } + } + } + + fun getColorUsageCount(chatColorsId: ChatColors.Id): Int { + val where = "$CUSTOM_CHAT_COLORS_ID = ?" + val args = SqlUtil.buildArgs(chatColorsId.longValue) + + readableDatabase.query(TABLE_NAME, arrayOf("COUNT(*)"), where, args, null, null, null).use { cursor -> + return if (cursor.moveToFirst()) { + cursor.getInt(0) + } else { + 0 + } + } + } + + fun clearAllColors() { + val database = writableDatabase + val where = "$CUSTOM_CHAT_COLORS_ID != ?" + val args = SqlUtil.buildArgs(ChatColors.Id.NotSet.longValue) + val toUpdate: MutableList = LinkedList() + + database.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + toUpdate.add(RecipientId.from(cursor.requireLong(ID))) + } + } + + if (toUpdate.isEmpty()) { + return + } + + val values = ContentValues().apply { + put(CHAT_COLORS, null as ByteArray?) + put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.longValue) + } + database.update(TABLE_NAME, values, where, args) + + for (id in toUpdate) { + Recipient.live(id).refresh() + } + } + + fun clearColor(id: RecipientId) { + val values = ContentValues().apply { + put(CHAT_COLORS, null as ByteArray?) + put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.longValue) + } + if (update(id, values)) { + Recipient.live(id).refresh() + } + } + + fun setColor(id: RecipientId, color: ChatColors) { + val values = ContentValues().apply { + put(CHAT_COLORS, color.serialize().toByteArray()) + put(CUSTOM_CHAT_COLORS_ID, color.id.longValue) + } + if (update(id, values)) { + Recipient.live(id).refresh() + } + } + + fun setDefaultSubscriptionId(id: RecipientId, defaultSubscriptionId: Int) { + val values = ContentValues().apply { + put(DEFAULT_SUBSCRIPTION_ID, defaultSubscriptionId) + } + if (update(id, values)) { + Recipient.live(id).refresh() + } + } + + fun setForceSmsSelection(id: RecipientId, forceSmsSelection: Boolean) { + val contentValues = ContentValues(1).apply { + put(FORCE_SMS_SELECTION, if (forceSmsSelection) 1 else 0) + } + if (update(id, contentValues)) { + Recipient.live(id).refresh() + } + } + + fun setBlocked(id: RecipientId, blocked: Boolean) { + val values = ContentValues().apply { + put(BLOCKED, if (blocked) 1 else 0) + } + if (update(id, values)) { + rotateStorageId(id) + Recipient.live(id).refresh() + } + } + + fun setMessageRingtone(id: RecipientId, notification: Uri?) { + val values = ContentValues().apply { + put(MESSAGE_RINGTONE, notification?.toString()) + } + if (update(id, values)) { + Recipient.live(id).refresh() + } + } + + fun setCallRingtone(id: RecipientId, ringtone: Uri?) { + val values = ContentValues().apply { + put(CALL_RINGTONE, ringtone?.toString()) + } + if (update(id, values)) { + Recipient.live(id).refresh() + } + } + + fun setMessageVibrate(id: RecipientId, enabled: VibrateState) { + val values = ContentValues().apply { + put(MESSAGE_VIBRATE, enabled.id) + } + if (update(id, values)) { + Recipient.live(id).refresh() + } + } + + fun setCallVibrate(id: RecipientId, enabled: VibrateState) { + val values = ContentValues().apply { + put(CALL_VIBRATE, enabled.id) + } + if (update(id, values)) { + Recipient.live(id).refresh() + } + } + + fun setMuted(id: RecipientId, until: Long) { + val values = ContentValues().apply { + put(MUTE_UNTIL, until) + } + + if (update(id, values)) { + rotateStorageId(id) + Recipient.live(id).refresh() + } + + StorageSyncHelper.scheduleSyncForDataChange() + } + + fun setMuted(ids: Collection, until: Long) { + val db = writableDatabase + + db.beginTransaction() + try { + val query = SqlUtil.buildCollectionQuery(ID, ids) + val values = ContentValues().apply { + put(MUTE_UNTIL, until) + } + + db.update(TABLE_NAME, values, query.where, query.whereArgs) + for (id in ids) { + rotateStorageId(id) + } + + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + + for (id in ids) { + Recipient.live(id).refresh() + } + + StorageSyncHelper.scheduleSyncForDataChange() + } + + fun setSeenFirstInviteReminder(id: RecipientId) { + setInsightsBannerTier(id, InsightsBannerTier.TIER_ONE) + } + + fun setSeenSecondInviteReminder(id: RecipientId) { + setInsightsBannerTier(id, InsightsBannerTier.TIER_TWO) + } + + fun setHasSentInvite(id: RecipientId) { + setSeenSecondInviteReminder(id) + } + + private fun setInsightsBannerTier(id: RecipientId, insightsBannerTier: InsightsBannerTier) { + val query = "$ID = ? AND $SEEN_INVITE_REMINDER < ?" + val args = arrayOf(id.serialize(), insightsBannerTier.toString()) + val values = ContentValues(1).apply { + put(SEEN_INVITE_REMINDER, insightsBannerTier.id) + } + + writableDatabase.update(TABLE_NAME, values, query, args) + Recipient.live(id).refresh() + } + + fun setExpireMessages(id: RecipientId, expiration: Int) { + val values = ContentValues(1).apply { + put(MESSAGE_EXPIRATION_TIME, expiration) + } + if (update(id, values)) { + Recipient.live(id).refresh() + } + } + + fun setUnidentifiedAccessMode(id: RecipientId, unidentifiedAccessMode: UnidentifiedAccessMode) { + val values = ContentValues(1).apply { + put(UNIDENTIFIED_ACCESS_MODE, unidentifiedAccessMode.mode) + } + if (update(id, values)) { + Recipient.live(id).refresh() + } + } + + fun setLastSessionResetTime(id: RecipientId, lastResetTime: DeviceLastResetTime) { + val values = ContentValues(1).apply { + put(LAST_SESSION_RESET, lastResetTime.toByteArray()) + } + update(id, values) + } + + fun getLastSessionResetTimes(id: RecipientId): DeviceLastResetTime { + readableDatabase.query(TABLE_NAME, arrayOf(LAST_SESSION_RESET), ID_WHERE, SqlUtil.buildArgs(id), null, null, null).use { cursor -> + if (cursor.moveToFirst()) { + return try { + val serialized = cursor.requireBlob(LAST_SESSION_RESET) + if (serialized != null) { + DeviceLastResetTime.parseFrom(serialized) + } else { + DeviceLastResetTime.newBuilder().build() + } + } catch (e: InvalidProtocolBufferException) { + Log.w(TAG, e) + DeviceLastResetTime.newBuilder().build() + } + } + } + + return DeviceLastResetTime.newBuilder().build() + } + + fun setBadges(id: RecipientId, badges: List) { + val badgeListBuilder = BadgeList.newBuilder() + for (badge in badges) { + badgeListBuilder.addBadges(toDatabaseBadge(badge!!)) + } + + val values = ContentValues(1).apply { + put(BADGES, badgeListBuilder.build().toByteArray()) + } + + if (update(id, values)) { + Recipient.live(id).refresh() + } + } + + fun setCapabilities(id: RecipientId, capabilities: SignalServiceProfile.Capabilities) { + var value: Long = 0 + value = Bitmask.update(value, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv2).serialize().toLong()) + value = Bitmask.update(value, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv1Migration).serialize().toLong()) + value = Bitmask.update(value, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isSenderKey).serialize().toLong()) + value = Bitmask.update(value, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isAnnouncementGroup).serialize().toLong()) + value = Bitmask.update(value, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isChangeNumber).serialize().toLong()) + + val values = ContentValues(1).apply { + put(CAPABILITIES, value) + } + + if (update(id, values)) { + Recipient.live(id).refresh() + } + } + + fun setMentionSetting(id: RecipientId, mentionSetting: MentionSetting) { + val values = ContentValues().apply { + put(MENTION_SETTING, mentionSetting.id) + } + if (update(id, values)) { + Recipient.live(id).refresh() + } + } + + /** + * Updates the profile key. + * + * If it changes, it clears out the profile key credential and resets the unidentified access mode. + * @return true iff changed. + */ + fun setProfileKey(id: RecipientId, profileKey: ProfileKey): Boolean { + val selection = "$ID = ?" + val args = arrayOf(id.serialize()) + val encodedProfileKey = Base64.encodeBytes(profileKey.serialize()) + val valuesToCompare = ContentValues(1).apply { + put(PROFILE_KEY, encodedProfileKey) + } + val valuesToSet = ContentValues(3).apply { + put(PROFILE_KEY, encodedProfileKey) + putNull(PROFILE_KEY_CREDENTIAL) + put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.mode) + } + + val updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare) + + if (update(updateQuery, valuesToSet)) { + rotateStorageId(id) + Recipient.live(id).refresh() + StorageSyncHelper.scheduleSyncForDataChange() + return true + } + return false + } + + /** + * Sets the profile key iff currently null. + * + * If it sets it, it also clears out the profile key credential and resets the unidentified access mode. + * @return true iff changed. + */ + fun setProfileKeyIfAbsent(id: RecipientId, profileKey: ProfileKey): Boolean { + val selection = "$ID = ? AND $PROFILE_KEY is NULL" + val args = arrayOf(id.serialize()) + val valuesToSet = ContentValues(3).apply { + put(PROFILE_KEY, Base64.encodeBytes(profileKey.serialize())) + putNull(PROFILE_KEY_CREDENTIAL) + put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.mode) + } + + if (writableDatabase.update(TABLE_NAME, valuesToSet, selection, args) > 0) { + rotateStorageId(id) + Recipient.live(id).refresh() + return true + } else { + return false + } + } + + /** + * Updates the profile key credential as long as the profile key matches. + */ + fun setProfileKeyCredential( + id: RecipientId, + profileKey: ProfileKey, + profileKeyCredential: ProfileKeyCredential + ): Boolean { + val selection = "$ID = ? AND $PROFILE_KEY = ?" + val args = arrayOf(id.serialize(), Base64.encodeBytes(profileKey.serialize())) + val columnData = ProfileKeyCredentialColumnData.newBuilder() + .setProfileKey(ByteString.copyFrom(profileKey.serialize())) + .setProfileKeyCredential(ByteString.copyFrom(profileKeyCredential.serialize())) + .build() + val values = ContentValues(1).apply { + put(PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(columnData.toByteArray())) + } + val updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values) + + val updated = update(updateQuery, values) + if (updated) { + Recipient.live(id).refresh() + } + + return updated + } + + private fun clearProfileKeyCredential(id: RecipientId) { + val values = ContentValues(1) + values.putNull(PROFILE_KEY_CREDENTIAL) + if (update(id, values)) { + rotateStorageId(id) + Recipient.live(id).refresh() + } + } + + /** + * Fills in gaps (nulls) in profile key knowledge from new profile keys. + * + * + * If from authoritative source, this will overwrite local, otherwise it will only write to the + * database if missing. + */ + fun persistProfileKeySet(profileKeySet: ProfileKeySet): Set { + val profileKeys = profileKeySet.profileKeys + val authoritativeProfileKeys = profileKeySet.authoritativeProfileKeys + val totalKeys = profileKeys.size + authoritativeProfileKeys.size + + if (totalKeys == 0) { + return emptySet() + } + + Log.i(TAG, "Persisting $totalKeys Profile keys, ${authoritativeProfileKeys.size} of which are authoritative") + + val updated = HashSet(totalKeys) + val selfId = Recipient.self().id + + for ((key, value) in profileKeys) { + val recipientId = getOrInsertFromAci(key) + if (setProfileKeyIfAbsent(recipientId, value)) { + Log.i(TAG, "Learned new profile key") + updated.add(recipientId) + } + } + + for ((key, value) in authoritativeProfileKeys) { + val recipientId = getOrInsertFromAci(key) + + if (selfId == recipientId) { + Log.i(TAG, "Seen authoritative update for self") + if (value != ProfileKeyUtil.getSelfProfileKey()) { + Log.w(TAG, "Seen authoritative update for self that didn't match local, scheduling storage sync") + StorageSyncHelper.scheduleSyncForDataChange() + } + } else { + Log.i(TAG, "Profile key from owner $recipientId") + if (setProfileKey(recipientId, value)) { + Log.i(TAG, "Learned new profile key from owner") + updated.add(recipientId) + } + } + } + + return updated + } + + fun getSimilarRecipientIds(recipient: Recipient): List { + val projection = SqlUtil.buildArgs(ID, "COALESCE(NULLIF($SYSTEM_JOINED_NAME, ''), NULLIF($PROFILE_JOINED_NAME, '')) AS checked_name") + val where = "checked_name = ?" + val arguments = SqlUtil.buildArgs(recipient.profileName.toString()) + + readableDatabase.query(TABLE_NAME, projection, where, arguments, null, null, null).use { cursor -> + if (cursor == null || cursor.count == 0) { + return emptyList() + } + val results: MutableList = ArrayList(cursor.count) + while (cursor.moveToNext()) { + results.add(RecipientId.from(cursor.requireLong(ID))) + } + return results + } + } + + fun setProfileName(id: RecipientId, profileName: ProfileName) { + val contentValues = ContentValues(1).apply { + put(PROFILE_GIVEN_NAME, profileName.givenName) + put(PROFILE_FAMILY_NAME, profileName.familyName) + put(PROFILE_JOINED_NAME, profileName.toString()) + } + if (update(id, contentValues)) { + rotateStorageId(id) + Recipient.live(id).refresh() + StorageSyncHelper.scheduleSyncForDataChange() + } + } + + fun setProfileAvatar(id: RecipientId, profileAvatar: String?) { + val contentValues = ContentValues(1).apply { + put(SIGNAL_PROFILE_AVATAR, profileAvatar) + } + if (update(id, contentValues)) { + Recipient.live(id).refresh() + if (id == Recipient.self().id) { + rotateStorageId(id) + StorageSyncHelper.scheduleSyncForDataChange() + } + } + } + + fun setAbout(id: RecipientId, about: String?, emoji: String?) { + val contentValues = ContentValues().apply { + put(ABOUT, about) + put(ABOUT_EMOJI, emoji) + } + + if (update(id, contentValues)) { + Recipient.live(id).refresh() + } + } + + fun setProfileSharing(id: RecipientId, enabled: Boolean) { + val contentValues = ContentValues(1).apply { + put(PROFILE_SHARING, if (enabled) 1 else 0) + } + val profiledUpdated = update(id, contentValues) + + if (profiledUpdated && enabled) { + val group = groups.getGroup(id) + if (group.isPresent) { + setHasGroupsInCommon(group.get().members) + } + } + + if (profiledUpdated) { + rotateStorageId(id) + Recipient.live(id).refresh() + StorageSyncHelper.scheduleSyncForDataChange() + } + } + + fun setNotificationChannel(id: RecipientId, notificationChannel: String?) { + val contentValues = ContentValues(1).apply { + put(NOTIFICATION_CHANNEL, notificationChannel) + } + if (update(id, contentValues)) { + Recipient.live(id).refresh() + } + } + + fun resetAllWallpaper() { + val database = writableDatabase + val selection = SqlUtil.buildArgs(ID, WALLPAPER_URI) + val where = "$WALLPAPER IS NOT NULL" + val idWithWallpaper: MutableList> = LinkedList() + + database.beginTransaction() + try { + database.query(TABLE_NAME, selection, where, null, null, null, null).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + idWithWallpaper.add( + Pair( + RecipientId.from(cursor.requireInt(ID).toLong()), + cursor.optionalString(WALLPAPER_URI).orNull() + ) + ) + } + } + + if (idWithWallpaper.isEmpty()) { + return + } + + val values = ContentValues(2).apply { + putNull(WALLPAPER_URI) + putNull(WALLPAPER) + } + + val rowsUpdated = database.update(TABLE_NAME, values, where, null) + if (rowsUpdated == idWithWallpaper.size) { + for (pair in idWithWallpaper) { + Recipient.live(pair.first()).refresh() + if (pair.second() != null) { + WallpaperStorage.onWallpaperDeselected(context, Uri.parse(pair.second())) + } + } + } else { + throw AssertionError("expected " + idWithWallpaper.size + " but got " + rowsUpdated) + } + } finally { + database.setTransactionSuccessful() + database.endTransaction() + } + } + + fun setWallpaper(id: RecipientId, chatWallpaper: ChatWallpaper?) { + setWallpaper(id, chatWallpaper?.serialize()) + } + + private fun setWallpaper(id: RecipientId, wallpaper: Wallpaper?) { + val existingWallpaperUri = getWallpaperUri(id) + val values = ContentValues().apply { + put(WALLPAPER, wallpaper?.toByteArray()) + if (wallpaper != null && wallpaper.hasFile()) { + put(WALLPAPER_URI, wallpaper.file.uri) + } else { + putNull(WALLPAPER_URI) + } + } + + if (update(id, values)) { + Recipient.live(id).refresh() + } + + if (existingWallpaperUri != null) { + WallpaperStorage.onWallpaperDeselected(context, existingWallpaperUri) + } + } + + fun setDimWallpaperInDarkTheme(id: RecipientId, enabled: Boolean) { + val wallpaper = getWallpaper(id) ?: throw IllegalStateException("No wallpaper set for $id") + val updated = wallpaper.toBuilder() + .setDimLevelInDarkTheme(if (enabled) ChatWallpaper.FIXED_DIM_LEVEL_FOR_DARK_THEME else 0f) + .build() + + setWallpaper(id, updated) + } + + private fun getWallpaper(id: RecipientId): Wallpaper? { + readableDatabase.query(TABLE_NAME, arrayOf(WALLPAPER), ID_WHERE, SqlUtil.buildArgs(id), null, null, null).use { cursor -> + if (cursor.moveToFirst()) { + val raw = cursor.requireBlob(WALLPAPER) + return if (raw != null) { + try { + Wallpaper.parseFrom(raw) + } catch (e: InvalidProtocolBufferException) { + null + } + } else { + null + } + } + } + + return null + } + + private fun getWallpaperUri(id: RecipientId): Uri? { + val wallpaper = getWallpaper(id) + + return if (wallpaper != null && wallpaper.hasFile()) { + Uri.parse(wallpaper.file.uri) + } else { + null + } + } + + fun getWallpaperUriUsageCount(uri: Uri): Int { + val query = "$WALLPAPER_URI = ?" + val args = SqlUtil.buildArgs(uri) + + readableDatabase.query(TABLE_NAME, arrayOf("COUNT(*)"), query, args, null, null, null).use { cursor -> + if (cursor.moveToFirst()) { + return cursor.getInt(0) + } + } + + return 0 + } + + /** + * @return True if setting the phone number resulted in changed recipientId, otherwise false. + */ + fun setPhoneNumber(id: RecipientId, e164: String): Boolean { + val db = writableDatabase + + db.beginTransaction() + return try { + setPhoneNumberOrThrow(id, e164) + db.setTransactionSuccessful() + false + } catch (e: SQLiteConstraintException) { + Log.w(TAG, "[setPhoneNumber] Hit a conflict when trying to update $id. Possibly merging.") + + val existing: RecipientRecord = getRecord(id) + val newId = getAndPossiblyMerge(existing.aci, e164, true) + Log.w(TAG, "[setPhoneNumber] Resulting id: $newId") + + db.setTransactionSuccessful() + newId != existing.id + } finally { + db.endTransaction() + } + } + + private fun removePhoneNumber(recipientId: RecipientId, db: SQLiteDatabase) { + val values = ContentValues().apply { + 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. + */ + fun setPhoneNumberOrThrow(id: RecipientId, e164: String) { + val contentValues = ContentValues(1).apply { + put(PHONE, e164) + } + if (update(id, contentValues)) { + rotateStorageId(id) + Recipient.live(id).refresh() + StorageSyncHelper.scheduleSyncForDataChange() + } + } + + fun updateSelfPhone(e164: String) { + val db = writableDatabase + + db.beginTransaction() + try { + val id = Recipient.self().id + val newId = getAndPossiblyMerge(Recipient.self().requireAci(), e164, highTrust = true, changeSelf = true) + + if (id == newId) { + Log.i(TAG, "[updateSelfPhone] Phone updated for self") + } else { + throw AssertionError("[updateSelfPhone] Self recipient id changed when updating phone. old: $id new: $newId") + } + + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + fun setUsername(id: RecipientId, username: String?) { + if (username != null) { + val existingUsername = getByUsername(username) + if (existingUsername.isPresent && id != existingUsername.get()) { + Log.i(TAG, "Username was previously thought to be owned by " + existingUsername.get() + ". Clearing their username.") + setUsername(existingUsername.get(), null) + } + } + + val contentValues = ContentValues(1).apply { + put(USERNAME, username) + } + if (update(id, contentValues)) { + Recipient.live(id).refresh() + StorageSyncHelper.scheduleSyncForDataChange() + } + } + + fun clearUsernameIfExists(username: String) { + val existingUsername = getByUsername(username) + if (existingUsername.isPresent) { + setUsername(existingUsername.get(), null) + } + } + + fun getAllPhoneNumbers(): Set { + val results: MutableSet = HashSet() + readableDatabase.query(TABLE_NAME, arrayOf(PHONE), null, null, null, null, null).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + val number = cursor.getString(cursor.getColumnIndexOrThrow(PHONE)) + if (!TextUtils.isEmpty(number)) { + results.add(number) + } + } + } + return results + } + + /** + * @return True if setting the UUID resulted in changed recipientId, otherwise false. + */ + fun markRegistered(id: RecipientId, aci: ACI): Boolean { + val db = writableDatabase + + db.beginTransaction() + try { + markRegisteredOrThrow(id, aci) + db.setTransactionSuccessful() + return false + } catch (e: SQLiteConstraintException) { + Log.w(TAG, "[markRegistered] Hit a conflict when trying to update $id. Possibly merging.") + + val existing = getRecord(id) + val newId = getAndPossiblyMerge(aci, existing.e164, true) + Log.w(TAG, "[markRegistered] Merged into $newId") + + db.setTransactionSuccessful() + return newId != existing.id + } finally { + db.endTransaction() + } + } + + /** + * Should only use if you are confident that this shouldn't result in any contact merging. + */ + fun markRegisteredOrThrow(id: RecipientId, aci: ACI) { + val contentValues = ContentValues(2).apply { + put(REGISTERED, RegisteredState.REGISTERED.id) + put(ACI_COLUMN, aci.toString().toLowerCase()) + } + if (update(id, contentValues)) { + setStorageIdIfNotSet(id) + Recipient.live(id).refresh() + } + } + + fun markUnregistered(id: RecipientId) { + val contentValues = ContentValues(2).apply { + put(REGISTERED, RegisteredState.NOT_REGISTERED.id) + putNull(STORAGE_SERVICE_ID) + } + if (update(id, contentValues)) { + Recipient.live(id).refresh() + } + } + + fun bulkUpdatedRegisteredStatus(registered: Map, unregistered: Collection) { + val db = writableDatabase + + db.beginTransaction() + try { + for ((recipientId, aci) in registered) { + val values = ContentValues(2).apply { + put(REGISTERED, RegisteredState.REGISTERED.id) + if (aci != null) { + put(ACI_COLUMN, aci.toString().toLowerCase()) + } + } + + try { + if (update(recipientId, values)) { + setStorageIdIfNotSet(recipientId) + } + } catch (e: SQLiteConstraintException) { + Log.w(TAG, "[bulkUpdateRegisteredStatus] Hit a conflict when trying to update $recipientId. Possibly merging.") + val e164 = getRecord(recipientId).e164 + val newId = getAndPossiblyMerge(aci, e164, true) + Log.w(TAG, "[bulkUpdateRegisteredStatus] Merged into $newId") + } + } + + for (id in unregistered) { + val values = ContentValues(2).apply { + put(REGISTERED, RegisteredState.NOT_REGISTERED.id) + putNull(STORAGE_SERVICE_ID) + } + update(id, values) + } + + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + /** + * Handles inserts the (e164, UUID) pairs, which could result in merges. Does not mark users as + * registered. + * + * @return A mapping of (RecipientId, UUID) + */ + fun bulkProcessCdsResult(mapping: Map): Map { + val db = writableDatabase + val aciMap: MutableMap = mutableMapOf() + + db.beginTransaction() + try { + for ((e164, aci) in mapping) { + var aciEntry = if (aci != null) getByAci(aci) else Optional.absent() + + if (aciEntry.isPresent) { + val idChanged = setPhoneNumber(aciEntry.get(), e164) + if (idChanged) { + aciEntry = getByAci(aci!!) + } + } + + val id = if (aciEntry.isPresent) aciEntry.get() else getOrInsertFromE164(e164) + aciMap[id] = aci + } + + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + + return aciMap + } + + fun getUninvitedRecipientsForInsights(): List { + val results: MutableList = LinkedList() + val args = arrayOf((System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31)).toString()) + + readableDatabase.rawQuery(INSIGHTS_INVITEE_LIST, args).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))) + } + } + + return results + } + + fun getRegistered(): List { + val results: MutableList = LinkedList() + + readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$REGISTERED = ?", arrayOf("1"), null, null, null).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))) + } + } + return results + } + + fun getSystemContacts(): List { + val results: MutableList = LinkedList() + + readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$SYSTEM_JOINED_NAME IS NOT NULL AND $SYSTEM_JOINED_NAME != \"\"", null, null, null, null).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))) + } + } + + return results + } + + /** + * We no longer automatically generate a chat color. This method is used only + * in the case of a legacy migration and otherwise should not be called. + */ + @Deprecated("") + fun updateSystemContactColors() { + val db = readableDatabase + val updates: MutableMap = HashMap() + + db.beginTransaction() + try { + db.query(TABLE_NAME, arrayOf(ID, "color", CHAT_COLORS, CUSTOM_CHAT_COLORS_ID, SYSTEM_JOINED_NAME), "$SYSTEM_JOINED_NAME IS NOT NULL AND $SYSTEM_JOINED_NAME != \"\"", null, null, null, null).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + val id = cursor.requireLong(ID) + val serializedColor = cursor.requireString("color") + val customChatColorsId = cursor.requireLong(CUSTOM_CHAT_COLORS_ID) + val serializedChatColors = cursor.requireBlob(CHAT_COLORS) + var chatColors: ChatColors? = if (serializedChatColors != null) { + try { + forChatColor(forLongValue(customChatColorsId), ChatColor.parseFrom(serializedChatColors)) + } catch (e: InvalidProtocolBufferException) { + null + } + } else { + null + } + + if (chatColors != null) { + return + } + + chatColors = if (serializedColor != null) { + try { + getChatColors(MaterialColor.fromSerialized(serializedColor)) + } catch (e: UnknownColorException) { + return + } + } else { + return + } + + val contentValues = ContentValues().apply { + put(CHAT_COLORS, chatColors.serialize().toByteArray()) + put(CUSTOM_CHAT_COLORS_ID, chatColors.id.longValue) + } + db.update(TABLE_NAME, contentValues, "$ID = ?", arrayOf(id.toString())) + updates[RecipientId.from(id)] = chatColors + } + } + } finally { + db.setTransactionSuccessful() + db.endTransaction() + updates.entries.forEach { Recipient.live(it.key).refresh() } + } + } + + fun getSignalContacts(includeSelf: Boolean): Cursor? { + val searchSelection = ContactSearchSelection.Builder() + .withRegistered(true) + .withGroups(false) + .excludeId(if (includeSelf) null else Recipient.self().id) + .build() + val selection = searchSelection.where + val args = searchSelection.args + val orderBy = "$SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $USERNAME, $PHONE" + return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy) + } + + fun querySignalContacts(inputQuery: String, includeSelf: Boolean): Cursor? { + val query = buildCaseInsensitiveGlobPattern(inputQuery) + + val searchSelection = ContactSearchSelection.Builder() + .withRegistered(true) + .withGroups(false) + .excludeId(if (includeSelf) null else Recipient.self().id) + .withSearchQuery(query) + .build() + val selection = searchSelection.where + val args = searchSelection.args + val orderBy = "$SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $PHONE" + + return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy) + } + + fun getNonSignalContacts(): Cursor? { + val searchSelection = ContactSearchSelection.Builder().withNonRegistered(true) + .withGroups(false) + .build() + val selection = searchSelection.where + val args = searchSelection.args + val orderBy = "$SYSTEM_JOINED_NAME, $PHONE" + return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy) + } + + fun queryNonSignalContacts(inputQuery: String): Cursor? { + val query = buildCaseInsensitiveGlobPattern(inputQuery) + val searchSelection = ContactSearchSelection.Builder() + .withNonRegistered(true) + .withGroups(false) + .withSearchQuery(query) + .build() + val selection = searchSelection.where + val args = searchSelection.args + val orderBy = "$SYSTEM_JOINED_NAME, $PHONE" + return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy) + } + + fun getNonGroupContacts(includeSelf: Boolean): Cursor? { + val searchSelection = ContactSearchSelection.Builder() + .withRegistered(true) + .withNonRegistered(true) + .withGroups(false) + .excludeId(if (includeSelf) null else Recipient.self().id) + .build() + val orderBy = orderByPreferringAlphaOverNumeric(SORT_NAME) + ", " + PHONE + return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, searchSelection.where, searchSelection.args, null, null, orderBy) + } + + fun queryNonGroupContacts(inputQuery: String, includeSelf: Boolean): Cursor? { + val query = buildCaseInsensitiveGlobPattern(inputQuery) + + val searchSelection = ContactSearchSelection.Builder() + .withRegistered(true) + .withNonRegistered(true) + .withGroups(false) + .excludeId(if (includeSelf) null else Recipient.self().id) + .withSearchQuery(query) + .build() + val selection = searchSelection.where + val args = searchSelection.args + val orderBy = orderByPreferringAlphaOverNumeric(SORT_NAME) + ", " + PHONE + + return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy) + } + + fun queryAllContacts(inputQuery: String): Cursor? { + val query = buildCaseInsensitiveGlobPattern(inputQuery) + val selection = + """ + $BLOCKED = ? AND + ( + $SORT_NAME GLOB ? OR + $USERNAME GLOB ? OR + $PHONE GLOB ? OR + $EMAIL GLOB ? + ) + """.trimIndent() + val args = SqlUtil.buildArgs("0", query, query, query, query) + return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null) + } + + @JvmOverloads + fun queryRecipientsForMentions(inputQuery: String, recipientIds: List? = null): List { + val query = buildCaseInsensitiveGlobPattern(inputQuery) + var ids: String? = null + + if (Util.hasItems(recipientIds)) { + ids = TextUtils.join(",", recipientIds?.map { it.serialize() }?.toList() ?: emptyList()) + } + + val selection = "$BLOCKED = 0 AND ${if (ids != null) "$ID IN ($ids) AND " else ""}$SORT_NAME GLOB ?" + val recipients: MutableList = ArrayList() + + RecipientReader(readableDatabase.query(TABLE_NAME, MENTION_SEARCH_PROJECTION, selection, SqlUtil.buildArgs(query), null, null, SORT_NAME)).use { reader -> + var recipient: Recipient? = reader.getNext() + while (recipient != null) { + recipients.add(recipient) + recipient = reader.getNext() + } + } + + return recipients + } + + fun getRecipientsForMultiDeviceSync(): List { + val subquery = "SELECT ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.RECIPIENT_ID} FROM ${ThreadDatabase.TABLE_NAME}" + val selection = "$REGISTERED = ? AND $GROUP_ID IS NULL AND $ID != ? AND ($SYSTEM_CONTACT_URI NOT NULL OR $ID IN ($subquery))" + val args = arrayOf(RegisteredState.REGISTERED.id.toString(), Recipient.self().id.serialize()) + val recipients: MutableList = ArrayList() + + readableDatabase.query(TABLE_NAME, ID_PROJECTION, selection, args, null, null, null).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + recipients.add(Recipient.resolved(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID))))) + } + } + return recipients + } + + /** + * @param lastInteractionThreshold Only include contacts that have been interacted with since this time. + * @param lastProfileFetchThreshold Only include contacts that haven't their profile fetched after this time. + * @param limit Only return at most this many contact. + */ + fun getRecipientsForRoutineProfileFetch(lastInteractionThreshold: Long, lastProfileFetchThreshold: Long, limit: Int): List { + val threadDatabase = threads + val recipientsWithinInteractionThreshold: MutableSet = LinkedHashSet() + + threadDatabase.readerFor(threadDatabase.getRecentPushConversationList(-1, false)).use { reader -> + var record: ThreadRecord? = reader.next + + while (record != null && record.date > lastInteractionThreshold) { + val recipient = Recipient.resolved(record.recipient.id) + if (recipient.isGroup) { + recipientsWithinInteractionThreshold.addAll(recipient.participants) + } else { + recipientsWithinInteractionThreshold.add(recipient) + } + record = reader.next + } + } + + return recipientsWithinInteractionThreshold + .filterNot { it.isSelf } + .filter { it.lastProfileFetchTime < lastProfileFetchThreshold } + .take(limit) + .map { it.id } + .toMutableList() + } + + fun markProfilesFetched(ids: Collection, time: Long) { + val db = writableDatabase + db.beginTransaction() + try { + val values = ContentValues(1).apply { + put(LAST_PROFILE_FETCH, time) + } + + for (id in ids) { + db.update(TABLE_NAME, values, ID_WHERE, arrayOf(id.serialize())) + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + fun applyBlockedUpdate(blocked: List, groupIds: List) { + val blockedE164 = blocked + .filter { b: SignalServiceAddress -> b.number.isPresent } + .map { b: SignalServiceAddress -> b.number.get() } + .toList() + + val blockedUuid = blocked + .map { b: SignalServiceAddress -> b.aci.toString().toLowerCase() } + .toList() + + val db = writableDatabase + db.beginTransaction() + try { + val resetBlocked = ContentValues().apply { + put(BLOCKED, 0) + } + db.update(TABLE_NAME, resetBlocked, null, null) + + val setBlocked = ContentValues().apply { + put(BLOCKED, 1) + put(PROFILE_SHARING, 0) + } + + for (e164 in blockedE164) { + db.update(TABLE_NAME, setBlocked, "$PHONE = ?", arrayOf(e164)) + } + + for (uuid in blockedUuid) { + db.update(TABLE_NAME, setBlocked, "$ACI_COLUMN = ?", arrayOf(uuid)) + } + + val groupIdStrings: MutableList = ArrayList(groupIds.size) + for (raw in groupIds) { + try { + groupIdStrings.add(GroupId.v1(raw)) + } catch (e: BadGroupIdException) { + Log.w(TAG, "[applyBlockedUpdate] Bad GV1 ID!") + } + } + + for (groupId in groupIdStrings) { + db.update(TABLE_NAME, setBlocked, "$GROUP_ID = ?", arrayOf(groupId.toString())) + } + + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + + ApplicationDependencies.getRecipientCache().clear() + } + + fun updateStorageId(recipientId: RecipientId, id: ByteArray?) { + updateStorageIds(Collections.singletonMap(recipientId, id)) + } + + private fun updateStorageIds(ids: Map) { + val db = writableDatabase + db.beginTransaction() + try { + for ((key, value) in ids) { + val values = ContentValues().apply { + put(STORAGE_SERVICE_ID, Base64.encodeBytes(value!!)) + } + db.update(TABLE_NAME, values, ID_WHERE, arrayOf(key.serialize())) + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + + for (id in ids.keys) { + Recipient.live(id).refresh() + } + } + + fun markPreMessageRequestRecipientsAsProfileSharingEnabled(messageRequestEnableTime: Long) { + val whereArgs = SqlUtil.buildArgs(messageRequestEnableTime, messageRequestEnableTime) + val select = + """ + SELECT r.$ID FROM $TABLE_NAME AS r + INNER JOIN ${ThreadDatabase.TABLE_NAME} AS t ON t.${ThreadDatabase.RECIPIENT_ID} = r.ID + WHERE + r.$PROFILE_SHARING = 0 AND ( + EXISTS(SELECT 1 FROM ${SmsDatabase.TABLE_NAME} WHERE ${SmsDatabase.THREAD_ID} = t.${ThreadDatabase.ID} AND ${SmsDatabase.DATE_RECEIVED} < ?) OR + EXISTS(SELECT 1 FROM ${MmsDatabase.TABLE_NAME} WHERE ${MmsDatabase.THREAD_ID} = t.${ThreadDatabase.ID} AND ${MmsDatabase.DATE_RECEIVED} < ?) + ) + """.trimIndent() + + val idsToUpdate: MutableList = ArrayList() + readableDatabase.rawQuery(select, whereArgs).use { cursor -> + while (cursor.moveToNext()) { + idsToUpdate.add(cursor.requireLong(ID)) + } + } + + if (Util.hasItems(idsToUpdate)) { + val query = SqlUtil.buildCollectionQuery(ID, idsToUpdate) + val values = ContentValues(1).apply { + put(PROFILE_SHARING, 1) + } + + writableDatabase.update(TABLE_NAME, values, query.where, query.whereArgs) + + for (id in idsToUpdate) { + Recipient.live(RecipientId.from(id)).refresh() + } + } + } + + fun setHasGroupsInCommon(recipientIds: List) { + if (recipientIds.isEmpty()) { + return + } + + var query = SqlUtil.buildCollectionQuery(ID, recipientIds) + val db = writableDatabase + + db.query(TABLE_NAME, arrayOf(ID), "${query.where} AND $GROUPS_IN_COMMON = 0", query.whereArgs, null, null, null).use { cursor -> + val idsToUpdate: MutableList = ArrayList(cursor.count) + + while (cursor.moveToNext()) { + idsToUpdate.add(cursor.requireLong(ID)) + } + + if (Util.hasItems(idsToUpdate)) { + query = SqlUtil.buildCollectionQuery(ID, idsToUpdate) + val values = ContentValues().apply { + put(GROUPS_IN_COMMON, 1) + } + + val count = db.update(TABLE_NAME, values, query.where, query.whereArgs) + if (count > 0) { + for (id in idsToUpdate) { + Recipient.live(RecipientId.from(id)).refresh() + } + } + } + } + } + + fun manuallyShowAvatar(recipientId: RecipientId) { + updateExtras(recipientId) { b: RecipientExtras.Builder -> b.setManuallyShownAvatar(true) } + } + + private fun updateExtras(recipientId: RecipientId, updater: java.util.function.Function) { + val db = writableDatabase + db.beginTransaction() + try { + db.query(TABLE_NAME, arrayOf(ID, EXTRAS), ID_WHERE, SqlUtil.buildArgs(recipientId), null, null, null).use { cursor -> + if (cursor.moveToNext()) { + val state = getRecipientExtras(cursor) + val builder = if (state != null) state.toBuilder() else RecipientExtras.newBuilder() + val updatedState = updater.apply(builder).build().toByteArray() + val values = ContentValues(1).apply { + put(EXTRAS, updatedState) + } + db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(cursor.requireLong(ID))) + } + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + Recipient.live(recipientId).refresh() + } + + /** + * Does not trigger any recipient refreshes -- it is assumed the caller handles this. + * Will *not* give storageIds to those that shouldn't get them (e.g. MMS groups, unregistered + * users). + */ + fun rotateStorageId(recipientId: RecipientId) { + val values = ContentValues(1).apply { + put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())) + } + + val query = "$ID = ? AND ($GROUP_TYPE IN (?, ?) OR $REGISTERED = ?)" + val args = SqlUtil.buildArgs(recipientId, GroupType.SIGNAL_V1.id, GroupType.SIGNAL_V2.id, RegisteredState.REGISTERED.id) + writableDatabase.update(TABLE_NAME, values, query, args) + } + + /** + * Does not trigger any recipient refreshes -- it is assumed the caller handles this. + */ + fun setStorageIdIfNotSet(recipientId: RecipientId) { + val values = ContentValues(1).apply { + put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())) + } + + val query = "$ID = ? AND $STORAGE_SERVICE_ID IS NULL" + val args = SqlUtil.buildArgs(recipientId) + writableDatabase.update(TABLE_NAME, values, query, args) + } + + /** + * Updates a group recipient with a new V2 group ID. Should only be done as a part of GV1->GV2 + * migration. + */ + fun updateGroupId(v1Id: V1, v2Id: V2) { + val values = ContentValues().apply { + put(GROUP_ID, v2Id.toString()) + put(GROUP_TYPE, GroupType.SIGNAL_V2.id) + } + + val query = SqlUtil.buildTrueUpdateQuery("$GROUP_ID = ?", SqlUtil.buildArgs(v1Id), values) + if (update(query, values)) { + val id = getByGroupId(v2Id).get() + rotateStorageId(id) + Recipient.live(id).refresh() + } + } + + /** + * Will update the database with the content values you specified. It will make an intelligent + * query such that this will only return true if a row was *actually* updated. + */ + private fun update(id: RecipientId, contentValues: ContentValues): Boolean { + val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(id), contentValues) + return update(updateQuery, contentValues) + } + + /** + * Will update the database with the {@param contentValues} you specified. + * + * + * This will only return true if a row was *actually* updated with respect to the where clause of the {@param updateQuery}. + */ + private fun update(updateQuery: SqlUtil.Query, contentValues: ContentValues): Boolean { + return writableDatabase.update(TABLE_NAME, contentValues, updateQuery.where, updateQuery.whereArgs) > 0 + } + + private fun getByColumn(column: String, value: String): Optional { + val query = "$column = ?" + val args = arrayOf(value) + + readableDatabase.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null).use { cursor -> + return if (cursor != null && cursor.moveToFirst()) { + Optional.of(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))) + } else { + Optional.absent() + } + } + } + + private fun getOrInsertByColumn(column: String, value: String): GetOrInsertResult { + if (TextUtils.isEmpty(value)) { + throw AssertionError("$column cannot be empty.") + } + + var existing = getByColumn(column, value) + + if (existing.isPresent) { + return GetOrInsertResult(existing.get(), false) + } else { + val values = ContentValues().apply { + put(column, value) + put(AVATAR_COLOR, AvatarColor.random().serialize()) + } + + val id = writableDatabase.insert(TABLE_NAME, null, values) + if (id < 0) { + existing = getByColumn(column, value) + if (existing.isPresent) { + return GetOrInsertResult(existing.get(), false) + } else { + throw AssertionError("Failed to insert recipient!") + } + } else { + return GetOrInsertResult(RecipientId.from(id), true) + } + } + } + + /** + * Merges one ACI recipient with an E164 recipient. It is assumed that the E164 recipient does + * *not* have an ACI. + */ + private fun merge(byAci: RecipientId, byE164: RecipientId): RecipientId { + ensureInTransaction() + val db = writableDatabase + val aciRecord = getRecord(byAci) + val e164Record = getRecord(byE164) + + // Identities + ApplicationDependencies.getIdentityStore().delete(e164Record.e164!!) + + // Group Receipts + val groupReceiptValues = ContentValues() + groupReceiptValues.put(GroupReceiptDatabase.RECIPIENT_ID, byAci.serialize()) + db.update(GroupReceiptDatabase.TABLE_NAME, groupReceiptValues, GroupReceiptDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164)) + + // Groups + val groupDatabase = groups + for (group in groupDatabase.getGroupsContainingMember(byE164, false, true)) { + val newMembers = LinkedHashSet(group.members).apply { + remove(byE164) + add(byAci) + } + + val groupValues = ContentValues().apply { + put(GroupDatabase.MEMBERS, RecipientId.toSerializedList(newMembers)) + } + db.update(GroupDatabase.TABLE_NAME, groupValues, GroupDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(group.recipientId)) + + if (group.isV2Group) { + groupDatabase.removeUnmigratedV1Members(group.id.requireV2(), listOf(byE164)) + } + } + + // Threads + val threadMerge = threads.merge(byAci, byE164) + + // SMS Messages + val smsValues = ContentValues().apply { + put(SmsDatabase.RECIPIENT_ID, byAci.serialize()) + } + db.update(SmsDatabase.TABLE_NAME, smsValues, SmsDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164)) + + if (threadMerge.neededMerge) { + val values = ContentValues().apply { + put(SmsDatabase.THREAD_ID, threadMerge.threadId) + } + db.update(SmsDatabase.TABLE_NAME, values, SmsDatabase.THREAD_ID + " = ?", SqlUtil.buildArgs(threadMerge.previousThreadId)) + } + + // MMS Messages + val mmsValues = ContentValues().apply { + put(MmsDatabase.RECIPIENT_ID, byAci.serialize()) + } + db.update(MmsDatabase.TABLE_NAME, mmsValues, MmsDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164)) + + if (threadMerge.neededMerge) { + val values = ContentValues() + values.put(MmsDatabase.THREAD_ID, threadMerge.threadId) + db.update(MmsDatabase.TABLE_NAME, values, MmsDatabase.THREAD_ID + " = ?", SqlUtil.buildArgs(threadMerge.previousThreadId)) + } + + // Sessions + val sessionDatabase = sessions + val hasE164Session = sessionDatabase.getAllFor(e164Record.e164).size > 0 + val hasAciSession = sessionDatabase.getAllFor(aciRecord.aci.toString()).size > 0 + + if (hasE164Session && hasAciSession) { + Log.w(TAG, "Had a session for both users. Deleting the E164.", true) + sessionDatabase.deleteAllFor(e164Record.e164) + } else if (hasE164Session && !hasAciSession) { + Log.w(TAG, "Had a session for E164, but not ACI. Re-assigning to the ACI.", true) + val values = ContentValues().apply { + put(SessionDatabase.ADDRESS, aciRecord.aci.toString()) + } + db.update(SessionDatabase.TABLE_NAME, values, SessionDatabase.ADDRESS + " = ?", SqlUtil.buildArgs(e164Record.e164)) + } else if (!hasE164Session && hasAciSession) { + Log.w(TAG, "Had a session for ACI, but not E164. No action necessary.", true) + } else { + Log.w(TAG, "Had no sessions. No action necessary.", true) + } + + // MSL + messageLog.remapRecipient(byE164, byAci) + + // Mentions + val mentionRecipientValues = ContentValues().apply { + put(MentionDatabase.RECIPIENT_ID, byAci.serialize()) + } + db.update(MentionDatabase.TABLE_NAME, mentionRecipientValues, MentionDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164)) + + if (threadMerge.neededMerge) { + val mentionThreadValues = ContentValues().apply { + put(MentionDatabase.THREAD_ID, threadMerge.threadId) + } + db.update(MentionDatabase.TABLE_NAME, mentionThreadValues, MentionDatabase.THREAD_ID + " = ?", SqlUtil.buildArgs(threadMerge.previousThreadId)) + } + threads.setLastScrolled(threadMerge.threadId, 0) + threads.update(threadMerge.threadId, false, false) + + // Reactions + reactions.remapRecipient(byE164, byAci) + + // Recipient + Log.w(TAG, "Deleting recipient $byE164", true) + db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164)) + RemappedRecords.getInstance().addRecipient(byE164, byAci) + + val uuidValues = ContentValues().apply { + put(PHONE, e164Record.e164) + put(BLOCKED, e164Record.isBlocked || aciRecord.isBlocked) + put(MESSAGE_RINGTONE, Optional.fromNullable(aciRecord.messageRingtone).or(Optional.fromNullable(e164Record.messageRingtone)).transform { obj: Uri? -> obj.toString() }.orNull()) + put(MESSAGE_VIBRATE, if (aciRecord.messageVibrateState != VibrateState.DEFAULT) aciRecord.messageVibrateState.id else e164Record.messageVibrateState.id) + put(CALL_RINGTONE, Optional.fromNullable(aciRecord.callRingtone).or(Optional.fromNullable(e164Record.callRingtone)).transform { obj: Uri? -> obj.toString() }.orNull()) + put(CALL_VIBRATE, if (aciRecord.callVibrateState != VibrateState.DEFAULT) aciRecord.callVibrateState.id else e164Record.callVibrateState.id) + put(NOTIFICATION_CHANNEL, aciRecord.notificationChannel ?: e164Record.notificationChannel) + put(MUTE_UNTIL, if (aciRecord.muteUntil > 0) aciRecord.muteUntil else e164Record.muteUntil) + put(CHAT_COLORS, Optional.fromNullable(aciRecord.chatColors).or(Optional.fromNullable(e164Record.chatColors)).transform { colors: ChatColors? -> colors!!.serialize().toByteArray() }.orNull()) + put(AVATAR_COLOR, aciRecord.avatarColor.serialize()) + put(CUSTOM_CHAT_COLORS_ID, Optional.fromNullable(aciRecord.chatColors).or(Optional.fromNullable(e164Record.chatColors)).transform { colors: ChatColors? -> colors!!.id.longValue }.orNull()) + put(SEEN_INVITE_REMINDER, e164Record.insightsBannerTier.id) + put(DEFAULT_SUBSCRIPTION_ID, e164Record.getDefaultSubscriptionId().or(-1)) + put(MESSAGE_EXPIRATION_TIME, if (aciRecord.expireMessages > 0) aciRecord.expireMessages else e164Record.expireMessages) + put(REGISTERED, RegisteredState.REGISTERED.id) + put(SYSTEM_GIVEN_NAME, e164Record.systemProfileName.givenName) + put(SYSTEM_FAMILY_NAME, e164Record.systemProfileName.familyName) + put(SYSTEM_JOINED_NAME, e164Record.systemProfileName.toString()) + put(SYSTEM_PHOTO_URI, e164Record.systemContactPhotoUri) + put(SYSTEM_PHONE_LABEL, e164Record.systemPhoneLabel) + put(SYSTEM_CONTACT_URI, e164Record.systemContactUri) + put(PROFILE_SHARING, aciRecord.profileSharing || e164Record.profileSharing) + put(CAPABILITIES, max(aciRecord.rawCapabilities, e164Record.rawCapabilities)) + put(MENTION_SETTING, if (aciRecord.mentionSetting != MentionSetting.ALWAYS_NOTIFY) aciRecord.mentionSetting.id else e164Record.mentionSetting.id) + } + + if (aciRecord.profileKey != null) { + updateProfileValuesForMerge(uuidValues, aciRecord) + } else if (e164Record.profileKey != null) { + updateProfileValuesForMerge(uuidValues, e164Record) + } + + db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(byAci)) + return byAci + } + + private fun ensureInTransaction() { + check(writableDatabase.inTransaction()) { "Must be in a transaction!" } + } + + private fun buildContentValuesForNewUser(e164: String?, aci: ACI?): ContentValues { + val values = ContentValues() + values.put(PHONE, e164) + if (aci != null) { + values.put(ACI_COLUMN, aci.toString().toLowerCase()) + values.put(REGISTERED, RegisteredState.REGISTERED.id) + values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())) + values.put(AVATAR_COLOR, AvatarColor.random().serialize()) + } + return values + } + + private fun getValuesForStorageContact(contact: SignalContactRecord, isInsert: Boolean): ContentValues { + return ContentValues().apply { + val profileName = ProfileName.fromParts(contact.givenName.orNull(), contact.familyName.orNull()) + val username = contact.username.orNull() + + if (contact.address.hasValidAci()) { + put(ACI_COLUMN, contact.address.aci.toString()) + } + + put(PHONE, contact.address.number.orNull()) + put(PROFILE_GIVEN_NAME, profileName.givenName) + put(PROFILE_FAMILY_NAME, profileName.familyName) + put(PROFILE_JOINED_NAME, profileName.toString()) + put(PROFILE_KEY, contact.profileKey.transform { source -> Base64.encodeBytes(source) }.orNull()) + put(USERNAME, if (TextUtils.isEmpty(username)) null else username) + put(PROFILE_SHARING, if (contact.isProfileSharingEnabled) "1" else "0") + put(BLOCKED, if (contact.isBlocked) "1" else "0") + put(MUTE_UNTIL, contact.muteUntil) + put(STORAGE_SERVICE_ID, Base64.encodeBytes(contact.id.raw)) + + if (contact.hasUnknownFields()) { + put(STORAGE_PROTO, Base64.encodeBytes(Objects.requireNonNull(contact.serializeUnknownFields()))) + } else { + putNull(STORAGE_PROTO) + } + + if (isInsert) { + put(AVATAR_COLOR, AvatarColor.random().serialize()) + } + } + } + + private fun getValuesForStorageGroupV1(groupV1: SignalGroupV1Record, isInsert: Boolean): ContentValues { + return ContentValues().apply { + put(GROUP_ID, GroupId.v1orThrow(groupV1.groupId).toString()) + put(GROUP_TYPE, GroupType.SIGNAL_V1.id) + put(PROFILE_SHARING, if (groupV1.isProfileSharingEnabled) "1" else "0") + put(BLOCKED, if (groupV1.isBlocked) "1" else "0") + put(MUTE_UNTIL, groupV1.muteUntil) + put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV1.id.raw)) + + if (groupV1.hasUnknownFields()) { + put(STORAGE_PROTO, Base64.encodeBytes(groupV1.serializeUnknownFields())) + } else { + putNull(STORAGE_PROTO) + } + + if (isInsert) { + put(AVATAR_COLOR, AvatarColor.random().serialize()) + } + } + } + + private fun getValuesForStorageGroupV2(groupV2: SignalGroupV2Record, isInsert: Boolean): ContentValues { + return ContentValues().apply { + put(GROUP_ID, GroupId.v2(groupV2.masterKeyOrThrow).toString()) + put(GROUP_TYPE, GroupType.SIGNAL_V2.id) + put(PROFILE_SHARING, if (groupV2.isProfileSharingEnabled) "1" else "0") + put(BLOCKED, if (groupV2.isBlocked) "1" else "0") + put(MUTE_UNTIL, groupV2.muteUntil) + put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV2.id.raw)) + + if (groupV2.hasUnknownFields()) { + put(STORAGE_PROTO, Base64.encodeBytes(groupV2.serializeUnknownFields())) + } else { + putNull(STORAGE_PROTO) + } + + if (isInsert) { + put(AVATAR_COLOR, AvatarColor.random().serialize()) + } + } + } + + fun getRecord(context: Context, cursor: Cursor): RecipientRecord { + return getRecord(context, cursor, ID) + } + + fun getRecord(context: Context, cursor: Cursor, idColumnName: String): RecipientRecord { + val profileKeyString = cursor.requireString(PROFILE_KEY) + val profileKeyCredentialString = cursor.requireString(PROFILE_KEY_CREDENTIAL) + var profileKey: ByteArray? = null + var profileKeyCredential: ProfileKeyCredential? = null + + if (profileKeyString != null) { + try { + profileKey = Base64.decode(profileKeyString) + } catch (e: IOException) { + Log.w(TAG, e) + } + + if (profileKeyCredentialString != null) { + try { + val columnDataBytes = Base64.decode(profileKeyCredentialString) + val columnData = ProfileKeyCredentialColumnData.parseFrom(columnDataBytes) + if (Arrays.equals(columnData.profileKey.toByteArray(), profileKey)) { + profileKeyCredential = ProfileKeyCredential(columnData.profileKeyCredential.toByteArray()) + } else { + Log.i(TAG, "Out of date profile key credential data ignored on read") + } + } catch (e: InvalidInputException) { + Log.w(TAG, "Profile key credential column data could not be read", e) + } catch (e: IOException) { + Log.w(TAG, "Profile key credential column data could not be read", e) + } + } + } + + val serializedWallpaper = cursor.requireBlob(WALLPAPER) + val chatWallpaper: ChatWallpaper? = if (serializedWallpaper != null) { + try { + ChatWallpaperFactory.create(Wallpaper.parseFrom(serializedWallpaper)) + } catch (e: InvalidProtocolBufferException) { + Log.w(TAG, "Failed to parse wallpaper.", e) + null + } + } else { + null + } + + val customChatColorsId = cursor.requireLong(CUSTOM_CHAT_COLORS_ID) + val serializedChatColors = cursor.requireBlob(CHAT_COLORS) + val chatColors: ChatColors? = if (serializedChatColors != null) { + try { + forChatColor(forLongValue(customChatColorsId), ChatColor.parseFrom(serializedChatColors)) + } catch (e: InvalidProtocolBufferException) { + Log.w(TAG, "Failed to parse chat colors.", e) + null + } + } else { + null + } + + val recipientId = RecipientId.from(cursor.requireLong(idColumnName)) + val capabilities = cursor.requireLong(CAPABILITIES) + + return RecipientRecord( + id = recipientId, + aci = ACI.parseOrNull(cursor.requireString(ACI_COLUMN)), + username = cursor.requireString(USERNAME), + e164 = cursor.requireString(PHONE), + email = cursor.requireString(EMAIL), + groupId = GroupId.parseNullableOrThrow(cursor.requireString(GROUP_ID)), + groupType = GroupType.fromId(cursor.requireInt(GROUP_TYPE)), + isBlocked = cursor.requireBoolean(BLOCKED), + muteUntil = cursor.requireLong(MUTE_UNTIL), + messageVibrateState = VibrateState.fromId(cursor.requireInt(MESSAGE_VIBRATE)), + callVibrateState = VibrateState.fromId(cursor.requireInt(CALL_VIBRATE)), + messageRingtone = Util.uri(cursor.requireString(MESSAGE_RINGTONE)), + callRingtone = Util.uri(cursor.requireString(CALL_RINGTONE)), + defaultSubscriptionId = cursor.requireInt(DEFAULT_SUBSCRIPTION_ID), + expireMessages = cursor.requireInt(MESSAGE_EXPIRATION_TIME), + registered = RegisteredState.fromId(cursor.requireInt(REGISTERED)), + profileKey = profileKey, + profileKeyCredential = profileKeyCredential, + systemProfileName = ProfileName.fromParts(cursor.requireString(SYSTEM_GIVEN_NAME), cursor.requireString(SYSTEM_FAMILY_NAME)), + systemDisplayName = cursor.requireString(SYSTEM_JOINED_NAME), + systemContactPhotoUri = cursor.requireString(SYSTEM_PHOTO_URI), + systemPhoneLabel = cursor.requireString(SYSTEM_PHONE_LABEL), + systemContactUri = cursor.requireString(SYSTEM_CONTACT_URI), + signalProfileName = ProfileName.fromParts(cursor.requireString(PROFILE_GIVEN_NAME), cursor.requireString(PROFILE_FAMILY_NAME)), + signalProfileAvatar = cursor.requireString(SIGNAL_PROFILE_AVATAR), + hasProfileImage = AvatarHelper.hasAvatar(context, recipientId), + profileSharing = cursor.requireBoolean(PROFILE_SHARING), + lastProfileFetch = cursor.requireLong(LAST_PROFILE_FETCH), + notificationChannel = cursor.requireString(NOTIFICATION_CHANNEL), + unidentifiedAccessMode = UnidentifiedAccessMode.fromMode(cursor.requireInt(UNIDENTIFIED_ACCESS_MODE)), + forceSmsSelection = cursor.requireBoolean(FORCE_SMS_SELECTION), + rawCapabilities = capabilities, + groupsV2Capability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH).toInt()), + groupsV1MigrationCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH).toInt()), + senderKeyCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH).toInt()), + announcementGroupCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH).toInt()), + changeNumberCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH).toInt()), + insightsBannerTier = InsightsBannerTier.fromId(cursor.requireInt(SEEN_INVITE_REMINDER)), + storageId = Base64.decodeNullableOrThrow(cursor.requireString(STORAGE_SERVICE_ID)), + mentionSetting = MentionSetting.fromId(cursor.requireInt(MENTION_SETTING)), + wallpaper = chatWallpaper, + chatColors = chatColors, + avatarColor = AvatarColor.deserialize(cursor.requireString(AVATAR_COLOR)), + about = cursor.requireString(ABOUT), + aboutEmoji = cursor.requireString(ABOUT_EMOJI), + syncExtras = getSyncExtras(cursor), + extras = getExtras(cursor), + hasGroupsInCommon = cursor.requireBoolean(GROUPS_IN_COMMON), + badges = parseBadgeList(cursor.requireBlob(BADGES)) + ) + } + + private fun parseBadgeList(serializedBadgeList: ByteArray?): List { + var badgeList: BadgeList? = null + if (serializedBadgeList != null) { + try { + badgeList = BadgeList.parseFrom(serializedBadgeList) + } catch (e: InvalidProtocolBufferException) { + Log.w(TAG, e) + } + } + + val badges: List + if (badgeList != null) { + val protoBadges = badgeList.badgesList + badges = ArrayList(protoBadges.size) + for (protoBadge in protoBadges) { + badges.add(Badges.fromDatabaseBadge(protoBadge)) + } + } else { + badges = emptyList() + } + + return badges + } + + private fun getSyncExtras(cursor: Cursor): RecipientRecord.SyncExtras { + val storageProtoRaw = cursor.optionalString(STORAGE_PROTO).orNull() + val storageProto = if (storageProtoRaw != null) Base64.decodeOrThrow(storageProtoRaw) else null + val archived = cursor.optionalBoolean(ThreadDatabase.ARCHIVED).or(false) + val forcedUnread = cursor.optionalInt(ThreadDatabase.READ).transform { status: Int -> status == ThreadDatabase.ReadStatus.FORCED_UNREAD.serialize() }.or(false) + val groupMasterKey = cursor.optionalBlob(GroupDatabase.V2_MASTER_KEY).transform { GroupUtil.requireMasterKey(it) }.orNull() + val identityKey = cursor.optionalString(IDENTITY_KEY).transform { Base64.decodeOrThrow(it) }.orNull() + val identityStatus = cursor.optionalInt(IDENTITY_STATUS).transform { VerifiedStatus.forState(it) }.or(VerifiedStatus.DEFAULT) + + return RecipientRecord.SyncExtras(storageProto, groupMasterKey, identityKey, identityStatus, archived, forcedUnread) + } + + private fun getExtras(cursor: Cursor): Recipient.Extras? { + return Recipient.Extras.from(getRecipientExtras(cursor)) + } + + private fun getRecipientExtras(cursor: Cursor): RecipientExtras? { + return cursor.optionalBlob(EXTRAS).transform { b: ByteArray? -> + try { + RecipientExtras.parseFrom(b) + } catch (e: InvalidProtocolBufferException) { + Log.w(TAG, e) + throw AssertionError(e) + } + }.orNull() + } + + /** + * Builds a case-insensitive GLOB pattern for fuzzy text queries. Works with all unicode + * characters. + * + * Ex: + * cat -> [cC][aA][tT] + */ + private fun buildCaseInsensitiveGlobPattern(query: String): String { + if (TextUtils.isEmpty(query)) { + return "*" + } + + val pattern = StringBuilder() + var i = 0 + val len = query.codePointCount(0, query.length) + while (i < len) { + val point = StringUtil.codePointToString(query.codePointAt(i)) + pattern.append("[") + pattern.append(point.toLowerCase()) + pattern.append(point.toUpperCase()) + pattern.append(getAccentuatedCharRegex(point.toLowerCase())) + pattern.append("]") + i++ + } + + return "*$pattern*" + } + + private fun getAccentuatedCharRegex(query: String): String { + return when (query) { + "a" -> "À-Åà-åĀ-ąǍǎǞ-ǡǺ-ǻȀ-ȃȦȧȺɐ-ɒḀḁẚẠ-ặ" + "b" -> "ßƀ-ƅɃɓḂ-ḇ" + "c" -> "çÇĆ-čƆ-ƈȻȼɔḈḉ" + "d" -> "ÐðĎ-đƉ-ƍȡɖɗḊ-ḓ" + "e" -> "È-Ëè-ëĒ-ěƎ-ƐǝȄ-ȇȨȩɆɇɘ-ɞḔ-ḝẸ-ệ" + "f" -> "ƑƒḞḟ" + "g" -> "Ĝ-ģƓǤ-ǧǴǵḠḡ" + "h" -> "Ĥ-ħƕǶȞȟḢ-ḫẖ" + "i" -> "Ì-Ïì-ïĨ-ıƖƗǏǐȈ-ȋɨɪḬ-ḯỈ-ị" + "j" -> "ĴĵǰȷɈɉɟ" + "k" -> "Ķ-ĸƘƙǨǩḰ-ḵ" + "l" -> "Ĺ-łƚȴȽɫ-ɭḶ-ḽ" + "m" -> "Ɯɯ-ɱḾ-ṃ" + "n" -> "ÑñŃ-ŋƝƞǸǹȠȵɲ-ɴṄ-ṋ" + "o" -> "Ò-ÖØò-öøŌ-őƟ-ơǑǒǪ-ǭǾǿȌ-ȏȪ-ȱṌ-ṓỌ-ợ" + "p" -> "ƤƥṔ-ṗ" + "q" -> "" + "r" -> "Ŕ-řƦȐ-ȓɌɍṘ-ṟ" + "s" -> "Ś-šƧƨȘșȿṠ-ṩ" + "t" -> "Ţ-ŧƫ-ƮȚțȾṪ-ṱẗ" + "u" -> "Ù-Üù-üŨ-ųƯ-ƱǓ-ǜȔ-ȗɄṲ-ṻỤ-ự" + "v" -> "ƲɅṼ-ṿ" + "w" -> "ŴŵẀ-ẉẘ" + "x" -> "Ẋ-ẍ" + "y" -> "ÝýÿŶ-ŸƔƳƴȲȳɎɏẎẏỲ-ỹỾỿẙ" + "z" -> "Ź-žƵƶɀẐ-ẕ" + "α" -> "\u0386\u0391\u03AC\u03B1\u1F00-\u1F0F\u1F70\u1F71\u1F80-\u1F8F\u1FB0-\u1FB4\u1FB6-\u1FBC" + "ε" -> "\u0388\u0395\u03AD\u03B5\u1F10-\u1F15\u1F18-\u1F1D\u1F72\u1F73\u1FC8\u1FC9" + "η" -> "\u0389\u0397\u03AE\u03B7\u1F20-\u1F2F\u1F74\u1F75\u1F90-\u1F9F\u1F20-\u1F2F\u1F74\u1F75\u1F90-\u1F9F\u1fc2\u1fc3\u1fc4\u1fc6\u1FC7\u1FCA\u1FCB\u1FCC" + "ι" -> "\u038A\u0390\u0399\u03AA\u03AF\u03B9\u03CA\u1F30-\u1F3F\u1F76\u1F77\u1FD0-\u1FD3\u1FD6-\u1FDB" + "ο" -> "\u038C\u039F\u03BF\u03CC\u1F40-\u1F45\u1F48-\u1F4D\u1F78\u1F79\u1FF8\u1FF9" + "σ" -> "\u03A3\u03C2\u03C3" + "ς" -> "\u03A3\u03C2\u03C3" + "υ" -> "\u038E\u03A5\u03AB\u03C5\u03CB\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F\u1F7A\u1F7B\u1FE0-\u1FE3\u1FE6-\u1FEB" + "ω" -> "\u038F\u03A9\u03C9\u03CE\u1F60-\u1F6F\u1F7C\u1F7D\u1FA0-\u1FAF\u1FF2-\u1FF4\u1FF6\u1FF7\u1FFA-\u1FFC" + else -> "" + } + } + + private fun updateProfileValuesForMerge(values: ContentValues, record: RecipientRecord) { + values.apply { + put(PROFILE_KEY, if (record.profileKey != null) Base64.encodeBytes(record.profileKey) else null) + putNull(PROFILE_KEY_CREDENTIAL) + put(SIGNAL_PROFILE_AVATAR, record.signalProfileAvatar) + put(PROFILE_GIVEN_NAME, record.signalProfileName.givenName) + put(PROFILE_FAMILY_NAME, record.signalProfileName.familyName) + put(PROFILE_JOINED_NAME, record.signalProfileName.toString()) + } + } + + /** + * By default, SQLite will prefer numbers over letters when sorting. e.g. (b, a, 1) is sorted as (1, a, b). + * This order by will using a GLOB pattern to instead sort it as (a, b, 1). + * + * @param column The name of the column to sort by + */ + private fun orderByPreferringAlphaOverNumeric(column: String): String { + return "CASE WHEN $column GLOB '[0-9]*' THEN 1 ELSE 0 END, $column" + } + + inner class BulkOperationsHandle internal constructor(private val database: SQLiteDatabase) { + private val pendingRecipients: MutableSet = mutableSetOf() + + fun setSystemContactInfo( + id: RecipientId, + systemProfileName: ProfileName, + systemDisplayName: String?, + photoUri: String?, + systemPhoneLabel: String?, + systemPhoneType: Int, + systemContactUri: String? + ) { + val joinedName = Util.firstNonNull(systemDisplayName, systemProfileName.toString()) + val refreshQualifyingValues = ContentValues().apply { + put(SYSTEM_GIVEN_NAME, systemProfileName.givenName) + put(SYSTEM_FAMILY_NAME, systemProfileName.familyName) + put(SYSTEM_JOINED_NAME, joinedName) + put(SYSTEM_PHOTO_URI, photoUri) + put(SYSTEM_PHONE_LABEL, systemPhoneLabel) + put(SYSTEM_PHONE_TYPE, systemPhoneType) + put(SYSTEM_CONTACT_URI, systemContactUri) + } + + val updatedValues = update(id, refreshQualifyingValues) + if (updatedValues) { + pendingRecipients.add(id) + } + + val otherValues = ContentValues().apply { + put(SYSTEM_INFO_PENDING, 0) + } + + update(id, otherValues) + } + + fun finish() { + markAllRelevantEntriesDirty() + clearSystemDataForPendingInfo() + database.setTransactionSuccessful() + database.endTransaction() + pendingRecipients.forEach { id -> Recipient.live(id).refresh() } + } + + private fun markAllRelevantEntriesDirty() { + val query = "$SYSTEM_INFO_PENDING = ? AND $STORAGE_SERVICE_ID NOT NULL" + val args = SqlUtil.buildArgs("1") + + database.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null).use { cursor -> + while (cursor.moveToNext()) { + val id = RecipientId.from(cursor.requireNonNullString(ID)) + rotateStorageId(id) + } + } + } + + private fun clearSystemDataForPendingInfo() { + val query = "$SYSTEM_INFO_PENDING = ?" + val args = arrayOf("1") + val values = ContentValues(5).apply { + put(SYSTEM_INFO_PENDING, 0) + put(SYSTEM_GIVEN_NAME, null as String?) + put(SYSTEM_FAMILY_NAME, null as String?) + put(SYSTEM_JOINED_NAME, null as String?) + put(SYSTEM_PHOTO_URI, null as String?) + put(SYSTEM_PHONE_LABEL, null as String?) + put(SYSTEM_CONTACT_URI, null as String?) + } + + database.update(TABLE_NAME, values, query, args) + } + } + + interface ColorUpdater { + fun update(name: String, materialColor: MaterialColor?): ChatColors? + } + + class RecipientReader internal constructor(private val cursor: Cursor) : Closeable { + + fun getCurrent(): Recipient { + val id = RecipientId.from(cursor.requireLong(ID)) + return Recipient.resolved(id) + } + + fun getNext(): Recipient? { + return if (cursor.moveToNext()) { + getCurrent() + } else { + null + } + } + + val count: Int + get() = cursor.count + + override fun close() { + cursor.close() + } + } + + class MissingRecipientException(id: RecipientId?) : IllegalStateException("Failed to find recipient with ID: $id") + + private class GetOrInsertResult(val recipientId: RecipientId, val neededInsert: Boolean) + + @VisibleForTesting + internal class ContactSearchSelection private constructor(val where: String, val args: Array) { + + @VisibleForTesting + internal class Builder { + private var includeRegistered = false + private var includeNonRegistered = false + private var excludeId: RecipientId? = null + private var excludeGroups = false + private var searchQuery: String? = null + + fun withRegistered(includeRegistered: Boolean): Builder { + this.includeRegistered = includeRegistered + return this + } + + fun withNonRegistered(includeNonRegistered: Boolean): Builder { + this.includeNonRegistered = includeNonRegistered + return this + } + + fun excludeId(recipientId: RecipientId?): Builder { + excludeId = recipientId + return this + } + + fun withGroups(includeGroups: Boolean): Builder { + excludeGroups = !includeGroups + return this + } + + fun withSearchQuery(searchQuery: String): Builder { + this.searchQuery = searchQuery + return this + } + + fun build(): ContactSearchSelection { + check(!(!includeRegistered && !includeNonRegistered)) { "Must include either registered or non-registered recipients in search" } + val stringBuilder = StringBuilder("(") + val args: MutableList = LinkedList() + + if (includeRegistered) { + stringBuilder.append("(") + args.add(RegisteredState.REGISTERED.id) + args.add(1) + if (Util.isEmpty(searchQuery)) { + stringBuilder.append(SIGNAL_CONTACT) + } else { + stringBuilder.append(QUERY_SIGNAL_CONTACT) + args.add(searchQuery) + args.add(searchQuery) + args.add(searchQuery) + } + stringBuilder.append(")") + } + + if (includeRegistered && includeNonRegistered) { + stringBuilder.append(" OR ") + } + + if (includeNonRegistered) { + stringBuilder.append("(") + args.add(RegisteredState.REGISTERED.id) + + if (Util.isEmpty(searchQuery)) { + stringBuilder.append(NON_SIGNAL_CONTACT) + } else { + stringBuilder.append(QUERY_NON_SIGNAL_CONTACT) + args.add(searchQuery) + args.add(searchQuery) + args.add(searchQuery) + } + + stringBuilder.append(")") + } + + stringBuilder.append(")") + stringBuilder.append(FILTER_BLOCKED) + args.add(0) + + if (excludeGroups) { + stringBuilder.append(FILTER_GROUPS) + } + + if (excludeId != null) { + stringBuilder.append(FILTER_ID) + args.add(excludeId!!.serialize()) + } + + return ContactSearchSelection(stringBuilder.toString(), args.map { obj: Any? -> obj.toString() }.toTypedArray()) + } + } + + companion object { + const val FILTER_GROUPS = " AND $GROUP_ID IS NULL" + const val FILTER_ID = " AND $ID != ?" + const val FILTER_BLOCKED = " AND $BLOCKED = ?" + const val NON_SIGNAL_CONTACT = "$REGISTERED != ? AND $SYSTEM_CONTACT_URI + NOT NULL AND ($PHONE NOT NULL OR $EMAIL NOT NULL)" + const val QUERY_NON_SIGNAL_CONTACT = "$NON_SIGNAL_CONTACT AND ($PHONE GLOB ? OR $EMAIL GLOB ? OR $SYSTEM_JOINED_NAME GLOB ?)" + const val SIGNAL_CONTACT = "$REGISTERED = ? AND (NULLIF($SYSTEM_JOINED_NAME, '') NOT NULL OR $PROFILE_SHARING = ?) AND ($SORT_NAME NOT NULL OR $USERNAME NOT NULL)" + const val QUERY_SIGNAL_CONTACT = "$SIGNAL_CONTACT AND ($PHONE GLOB ? OR $SORT_NAME GLOB ? OR $USERNAME GLOB ?)" + } + } + + /** + * Values that represent the index in the capabilities bitmask. Each index can store a 2-bit + * value, which in this case is the value of [Recipient.Capability]. + */ + internal object Capabilities { + const val BIT_LENGTH = 2 + const val GROUPS_V2 = 0 + const val GROUPS_V1_MIGRATION = 1 + const val SENDER_KEY = 2 + const val ANNOUNCEMENT_GROUPS = 3 + const val CHANGE_NUMBER = 4 + } + + enum class VibrateState(val id: Int) { + DEFAULT(0), ENABLED(1), DISABLED(2); + + companion object { + fun fromId(id: Int): VibrateState { + return values()[id] + } + + fun fromBoolean(enabled: Boolean): VibrateState { + return if (enabled) ENABLED else DISABLED + } + } + } + + enum class RegisteredState(val id: Int) { + UNKNOWN(0), REGISTERED(1), NOT_REGISTERED(2); + + companion object { + fun fromId(id: Int): RegisteredState { + return values()[id] + } + } + } + + enum class UnidentifiedAccessMode(val mode: Int) { + UNKNOWN(0), DISABLED(1), ENABLED(2), UNRESTRICTED(3); + + companion object { + fun fromMode(mode: Int): UnidentifiedAccessMode { + return values()[mode] + } + } + } + + enum class InsightsBannerTier(val id: Int) { + NO_TIER(0), TIER_ONE(1), TIER_TWO(2); + + fun seen(tier: InsightsBannerTier): Boolean { + return tier.id <= id + } + + companion object { + fun fromId(id: Int): InsightsBannerTier { + return values()[id] + } + } + } + + enum class GroupType(val id: Int) { + NONE(0), MMS(1), SIGNAL_V1(2), SIGNAL_V2(3); + + companion object { + fun fromId(id: Int): GroupType { + return values()[id] + } + } + } + + enum class MentionSetting(val id: Int) { + ALWAYS_NOTIFY(0), DO_NOT_NOTIFY(1); + + companion object { + fun fromId(id: Int): MentionSetting { + return values()[id] + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 29f10d4e39..49f4b36f23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -35,10 +35,10 @@ import org.signal.core.util.logging.Log; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo; -import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; +import org.thoughtcrime.securesms.database.model.RecipientRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.groups.BadGroupIdException; import org.thoughtcrime.securesms.groups.GroupId; @@ -1561,8 +1561,8 @@ public class ThreadDatabase extends Database { } public ThreadRecord getCurrent() { - RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, ThreadDatabase.RECIPIENT_ID)); - RecipientSettings recipientSettings = RecipientDatabase.getRecipientSettings(context, cursor, ThreadDatabase.RECIPIENT_ID); + RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, ThreadDatabase.RECIPIENT_ID)); + RecipientRecord recipientSettings = SignalDatabase.recipients().getRecord(context, cursor, ThreadDatabase.RECIPIENT_ID); Recipient recipient; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt new file mode 100644 index 0000000000..536d550a4a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt @@ -0,0 +1,102 @@ +package org.thoughtcrime.securesms.database.model + +import android.net.Uri +import org.signal.zkgroup.groups.GroupMasterKey +import org.signal.zkgroup.profiles.ProfileKeyCredential +import org.thoughtcrime.securesms.badges.models.Badge +import org.thoughtcrime.securesms.conversation.colors.AvatarColor +import org.thoughtcrime.securesms.conversation.colors.ChatColors +import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus +import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier +import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting +import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState +import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode +import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState +import org.thoughtcrime.securesms.database.model.RecipientRecord.SyncExtras +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.profiles.ProfileName +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.wallpaper.ChatWallpaper +import org.whispersystems.libsignal.util.guava.Optional +import org.whispersystems.signalservice.api.push.ACI + +/** + * Database model for [RecipientDatabase]. + */ +data class RecipientRecord( + val id: RecipientId, + val aci: ACI?, + val username: String?, + val e164: String?, + val email: String?, + val groupId: GroupId?, + val groupType: RecipientDatabase.GroupType, + val isBlocked: Boolean, + val muteUntil: Long, + val messageVibrateState: VibrateState, + val callVibrateState: VibrateState, + val messageRingtone: Uri?, + val callRingtone: Uri?, + private val defaultSubscriptionId: Int, + val expireMessages: Int, + val registered: RegisteredState, + val profileKey: ByteArray?, + val profileKeyCredential: ProfileKeyCredential?, + val systemProfileName: ProfileName, + val systemDisplayName: String?, + val systemContactPhotoUri: String?, + val systemPhoneLabel: String?, + val systemContactUri: String?, + @get:JvmName("getProfileName") + val signalProfileName: ProfileName, + @get:JvmName("getProfileAvatar") + val signalProfileAvatar: String?, + @get:JvmName("hasProfileImage") + val hasProfileImage: Boolean, + @get:JvmName("isProfileSharing") + val profileSharing: Boolean, + val lastProfileFetch: Long, + val notificationChannel: String?, + val unidentifiedAccessMode: UnidentifiedAccessMode, + @get:JvmName("isForceSmsSelection") + val forceSmsSelection: Boolean, + val rawCapabilities: Long, + val groupsV2Capability: Recipient.Capability, + val groupsV1MigrationCapability: Recipient.Capability, + val senderKeyCapability: Recipient.Capability, + val announcementGroupCapability: Recipient.Capability, + val changeNumberCapability: Recipient.Capability, + val insightsBannerTier: InsightsBannerTier, + val storageId: ByteArray?, + val mentionSetting: MentionSetting, + val wallpaper: ChatWallpaper?, + val chatColors: ChatColors?, + val avatarColor: AvatarColor, + val about: String?, + val aboutEmoji: String?, + val syncExtras: SyncExtras, + val extras: Recipient.Extras?, + @get:JvmName("hasGroupsInCommon") + val hasGroupsInCommon: Boolean, + val badges: List +) { + + fun getDefaultSubscriptionId(): Optional { + return if (defaultSubscriptionId != -1) Optional.of(defaultSubscriptionId) else Optional.absent() + } + + /** + * A bundle of data that's only necessary when syncing to storage service, not for a + * [Recipient]. + */ + data class SyncExtras( + val storageProto: ByteArray?, + val groupMasterKey: GroupMasterKey?, + val identityKey: ByteArray?, + val identityStatus: VerifiedStatus, + val isArchived: Boolean, + val isForcedUnread: Boolean + ) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java index fc79556a8a..a4e7fd3573 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java @@ -82,7 +82,7 @@ public class StorageForcePushJob extends BaseJob { long newVersion = currentVersion + 1; Map newContactStorageIds = generateContactStorageIds(oldContactStorageIds); List inserts = Stream.of(oldContactStorageIds.keySet()) - .map(recipientDatabase::getRecipientSettingsForSync) + .map(recipientDatabase::getRecordForSync) .withoutNulls() .map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newContactStorageIds.get(s.getId())).getRaw())) .toList(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java index f20af47d90..61f83bd114 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java @@ -10,7 +10,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; +import org.thoughtcrime.securesms.database.model.RecipientRecord; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.UnknownStorageIdDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -391,7 +391,7 @@ public class StorageSyncJob extends BaseJob { case ManifestRecord.Identifier.Type.CONTACT_VALUE: case ManifestRecord.Identifier.Type.GROUPV1_VALUE: case ManifestRecord.Identifier.Type.GROUPV2_VALUE: - RecipientSettings settings = recipientDatabase.getByStorageId(id.getRaw()); + RecipientRecord settings = recipientDatabase.getByStorageId(id.getRaw()); if (settings != null) { if (settings.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V2 && settings.getSyncExtras().getGroupMasterKey() == null) { throw new MissingGv2MasterKeyError(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplyUnknownFieldsToSelfMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplyUnknownFieldsToSelfMigrationJob.java index d20abcaa95..59abff9b20 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplyUnknownFieldsToSelfMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplyUnknownFieldsToSelfMigrationJob.java @@ -6,6 +6,7 @@ import com.google.protobuf.InvalidProtocolBufferException; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.RecipientDatabase; +import org.thoughtcrime.securesms.database.model.RecipientRecord; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; @@ -50,12 +51,12 @@ public class ApplyUnknownFieldsToSelfMigrationJob extends MigrationJob { return; } - Recipient self; - RecipientDatabase.RecipientSettings settings; + Recipient self; + RecipientRecord settings; try { self = Recipient.self(); - settings = SignalDatabase.recipients().getRecipientSettingsForSync(self.getId()); + settings = SignalDatabase.recipients().getRecordForSync(self.getId()); } catch (RecipientDatabase.MissingRecipientException e) { Log.w(TAG, "Unable to find self"); return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java index a10552b247..2b891df274 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java @@ -16,7 +16,7 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; +import org.thoughtcrime.securesms.database.model.RecipientRecord; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.whispersystems.libsignal.util.guava.Optional; @@ -192,7 +192,7 @@ public final class LiveRecipient { } private @NonNull Recipient fetchAndCacheRecipientFromDisk(@NonNull RecipientId id) { - RecipientSettings settings = recipientDatabase.getRecipientSettings(id); + RecipientRecord settings = recipientDatabase.getRecord(id); RecipientDetails details = settings.getGroupId() != null ? getGroupRecipientDetails(settings) : RecipientDetails.forIndividual(context, settings); @@ -202,7 +202,7 @@ public final class LiveRecipient { } @WorkerThread - private @NonNull RecipientDetails getGroupRecipientDetails(@NonNull RecipientSettings settings) { + private @NonNull RecipientDetails getGroupRecipientDetails(@NonNull RecipientRecord settings) { Optional groupRecord = groupDatabase.getGroup(settings.getId()); if (groupRecord.isPresent()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java index ffeecbb1c0..22db549da7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java @@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.conversation.colors.AvatarColor; import org.thoughtcrime.securesms.conversation.colors.ChatColors; import org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier; import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting; -import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; +import org.thoughtcrime.securesms.database.model.RecipientRecord; import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState; import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode; import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState; @@ -88,7 +88,7 @@ public class RecipientDetails { boolean systemContact, boolean isSelf, @NonNull RegisteredState registeredState, - @NonNull RecipientSettings settings, + @NonNull RecipientRecord settings, @Nullable List participants) { this.groupAvatarId = groupAvatarId; @@ -199,7 +199,7 @@ public class RecipientDetails { this.badges = Collections.emptyList(); } - public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientSettings settings) { + public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientRecord settings) { boolean systemContact = !settings.getSystemProfileName().isEmpty(); boolean isSelf = (settings.getE164() != null && settings.getE164().equals(SignalStore.account().getE164())) || (settings.getAci() != null && settings.getAci().equals(SignalStore.account().getAci())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.java index eb41d212a3..1533fc751c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.java @@ -7,6 +7,7 @@ import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.RecipientDatabase; +import org.thoughtcrime.securesms.database.model.RecipientRecord; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -69,7 +70,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor byUuid = recipientDatabase.getByAci(address.getAci()); Optional byE164 = address.getNumber().isPresent() ? recipientDatabase.getByE164(address.getNumber().get()) : Optional.absent(); - return byUuid.or(byE164).transform(recipientDatabase::getRecipientSettingsForSync) + return byUuid.or(byE164).transform(recipientDatabase::getRecordForSync) .transform(settings -> { if (settings.getStorageId() != null) { return StorageSyncModels.localToRemoteRecord(settings); @@ -77,7 +78,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor recipientId = recipientDatabase.getByGroupId(groupId); - return recipientId.transform(recipientDatabase::getRecipientSettingsForSync) + return recipientId.transform(recipientDatabase::getRecordForSync) .transform(StorageSyncModels::localToRemoteRecord) .transform(r -> r.getGroupV1().get()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java index f88250cc5f..9606a728bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java @@ -51,7 +51,7 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor< Optional recipientId = recipientDatabase.getByGroupId(groupId); - return recipientId.transform(recipientDatabase::getRecipientSettingsForSync) + return recipientId.transform(recipientDatabase::getRecordForSync) .transform(settings -> { if (settings.getSyncExtras().getGroupMasterKey() != null) { return StorageSyncModels.localToRemoteRecord(settings); diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java index 4e61282b36..c3cbca91b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java @@ -11,7 +11,7 @@ import com.annimon.stream.Stream; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; +import org.thoughtcrime.securesms.database.model.RecipientRecord; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob; @@ -105,20 +105,20 @@ public final class StorageSyncHelper { } public static SignalStorageRecord buildAccountRecord(@NonNull Context context, @NonNull Recipient self) { - RecipientDatabase recipientDatabase = SignalDatabase.recipients(); - RecipientSettings settings = recipientDatabase.getRecipientSettingsForSync(self.getId()); - List pinned = Stream.of(SignalDatabase.threads().getPinnedRecipientIds()) - .map(recipientDatabase::getRecipientSettingsForSync) - .toList(); + RecipientDatabase recipientDatabase = SignalDatabase.recipients(); + RecipientRecord record = recipientDatabase.getRecordForSync(self.getId()); + List pinned = Stream.of(SignalDatabase.threads().getPinnedRecipientIds()) + .map(recipientDatabase::getRecordForSync) + .toList(); SignalAccountRecord account = new SignalAccountRecord.Builder(self.getStorageServiceId()) - .setUnknownFields(settings != null ? settings.getSyncExtras().getStorageProto() : null) + .setUnknownFields(record != null ? record.getSyncExtras().getStorageProto() : null) .setProfileKey(self.getProfileKey()) .setGivenName(self.getProfileName().getGivenName()) .setFamilyName(self.getProfileName().getFamilyName()) .setAvatarUrlPath(self.getProfileAvatar()) - .setNoteToSelfArchived(settings != null && settings.getSyncExtras().isArchived()) - .setNoteToSelfForcedUnread(settings != null && settings.getSyncExtras().isForcedUnread()) + .setNoteToSelfArchived(record != null && record.getSyncExtras().isArchived()) + .setNoteToSelfForcedUnread(record != null && record.getSyncExtras().isForcedUnread()) .setTypingIndicatorsEnabled(TextSecurePreferences.isTypingIndicatorsEnabled(context)) .setReadReceiptsEnabled(TextSecurePreferences.isReadReceiptsEnabled(context)) .setSealedSenderIndicatorsEnabled(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java index 88cf4c51e8..cdc5ddcdd7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java @@ -8,7 +8,7 @@ import com.annimon.stream.Stream; import org.signal.zkgroup.groups.GroupMasterKey; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; +import org.thoughtcrime.securesms.database.model.RecipientRecord; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues; import org.thoughtcrime.securesms.subscription.Subscriber; @@ -29,7 +29,7 @@ public final class StorageSyncModels { private StorageSyncModels() {} - public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings) { + public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientRecord settings) { if (settings.getStorageId() == null) { throw new AssertionError("Must have a storage key!"); } @@ -37,7 +37,7 @@ public final class StorageSyncModels { return localToRemoteRecord(settings, settings.getStorageId()); } - public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings, @NonNull GroupMasterKey groupMasterKey) { + public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientRecord settings, @NonNull GroupMasterKey groupMasterKey) { if (settings.getStorageId() == null) { throw new AssertionError("Must have a storage key!"); } @@ -45,7 +45,7 @@ public final class StorageSyncModels { return SignalStorageRecord.forGroupV2(localToRemoteGroupV2(settings, settings.getStorageId(), groupMasterKey)); } - public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings, @NonNull byte[] rawStorageId) { + public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientRecord settings, @NonNull byte[] rawStorageId) { switch (settings.getGroupType()) { case NONE: return SignalStorageRecord.forContact(localToRemoteContact(settings, rawStorageId)); case SIGNAL_V1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, rawStorageId)); @@ -72,7 +72,7 @@ public final class StorageSyncModels { } } - public static List localToRemotePinnedConversations(@NonNull List settings) { + public static List localToRemotePinnedConversations(@NonNull List settings) { return Stream.of(settings) .filter(s -> s.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V1 || s.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V2 || @@ -81,7 +81,7 @@ public final class StorageSyncModels { .toList(); } - private static @NonNull SignalAccountRecord.PinnedConversation localToRemotePinnedConversation(@NonNull RecipientSettings settings) { + private static @NonNull SignalAccountRecord.PinnedConversation localToRemotePinnedConversation(@NonNull RecipientRecord settings) { switch (settings.getGroupType()) { case NONE : return SignalAccountRecord.PinnedConversation.forContact(new SignalServiceAddress(settings.getAci(), settings.getE164())); case SIGNAL_V1: return SignalAccountRecord.PinnedConversation.forGroupV1(settings.getGroupId().requireV1().getDecodedId()); @@ -90,7 +90,7 @@ public final class StorageSyncModels { } } - private static @NonNull SignalContactRecord localToRemoteContact(@NonNull RecipientSettings recipient, byte[] rawStorageId) { + private static @NonNull SignalContactRecord localToRemoteContact(@NonNull RecipientRecord recipient, byte[] rawStorageId) { if (recipient.getAci() == null && recipient.getE164() == null) { throw new AssertionError("Must have either a UUID or a phone number!"); } @@ -112,7 +112,7 @@ public final class StorageSyncModels { .build(); } - private static @NonNull SignalGroupV1Record localToRemoteGroupV1(@NonNull RecipientSettings recipient, byte[] rawStorageId) { + private static @NonNull SignalGroupV1Record localToRemoteGroupV1(@NonNull RecipientRecord recipient, byte[] rawStorageId) { GroupId groupId = recipient.getGroupId(); if (groupId == null) { @@ -133,7 +133,7 @@ public final class StorageSyncModels { .build(); } - private static @NonNull SignalGroupV2Record localToRemoteGroupV2(@NonNull RecipientSettings recipient, byte[] rawStorageId, @NonNull GroupMasterKey groupMasterKey) { + private static @NonNull SignalGroupV2Record localToRemoteGroupV2(@NonNull RecipientRecord recipient, byte[] rawStorageId, @NonNull GroupMasterKey groupMasterKey) { GroupId groupId = recipient.getGroupId(); if (groupId == null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Base64.java b/app/src/main/java/org/thoughtcrime/securesms/util/Base64.java index 2ad8aed799..beeb5d9b53 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Base64.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Base64.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.util; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.io.IOException; @@ -24,4 +25,16 @@ public final class Base64 { throw new AssertionError(); } } + + public static @Nullable byte[] decodeNullableOrThrow(@Nullable String s) { + if (s == null) { + return null; + } + + try { + return org.whispersystems.util.Base64.decode(s); + } catch (IOException e) { + throw new AssertionError(); + } + } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt index 83e5937f83..1c2ba5fa15 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt @@ -5,11 +5,13 @@ import org.signal.zkgroup.profiles.ProfileKeyCredential import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.conversation.colors.ChatColors +import org.thoughtcrime.securesms.database.model.RecipientRecord import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.profiles.ProfileName import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientDetails import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.util.Bitmask import org.thoughtcrime.securesms.wallpaper.ChatWallpaper import org.whispersystems.libsignal.util.guava.Optional import org.whispersystems.signalservice.api.push.ACI @@ -68,7 +70,7 @@ object RecipientDatabaseTestUtils { avatarColor: AvatarColor = AvatarColor.A100, about: String? = null, aboutEmoji: String? = null, - syncExtras: RecipientDatabase.RecipientSettings.SyncExtras = RecipientDatabase.RecipientSettings.SyncExtras( + syncExtras: RecipientRecord.SyncExtras = RecipientRecord.SyncExtras( null, null, null, @@ -88,7 +90,7 @@ object RecipientDatabaseTestUtils { systemContact, isSelf, registered, - RecipientDatabase.RecipientSettings( + RecipientRecord( recipientId, aci, username, @@ -121,6 +123,11 @@ object RecipientDatabaseTestUtils { unidentifiedAccessMode, forceSmsSelection, capabilities, + Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.GROUPS_V2, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), + Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.GROUPS_V1_MIGRATION, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), + Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.SENDER_KEY, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), + Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.ANNOUNCEMENT_GROUPS, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), + Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.CHANGE_NUMBER, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), insightBannerTier, storageId, mentionSetting, diff --git a/donations/app/src/main/java/org/signal/donations/app/MainActivity.java b/donations/app/src/main/java/org/signal/donations/app/MainActivity.java index 0ca607796b..747ab5a2da 100644 --- a/donations/app/src/main/java/org/signal/donations/app/MainActivity.java +++ b/donations/app/src/main/java/org/signal/donations/app/MainActivity.java @@ -79,15 +79,15 @@ public class MainActivity extends AppCompatActivity implements GooglePayApi.Paym donateButton.setClickable(true); } - @Override - public void onCancelled() { - Toast.makeText(this, "CANCELLED", Toast.LENGTH_SHORT).show(); - donateButton.setClickable(true); - } - @Override public void onError(@NonNull GooglePayApi.GooglePayException googlePayException) { Toast.makeText(this, "ERROR", Toast.LENGTH_SHORT).show(); donateButton.setClickable(true); } + + @Override + public void onCancelled() { + Toast.makeText(this, "CANCELLED", Toast.LENGTH_SHORT).show(); + donateButton.setClickable(true); + } }