From 69003dfbe2c3ff05eb6e8eadc38c9230cc2c336b Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 8 Dec 2022 09:05:32 -0500 Subject: [PATCH] Convert IdentityTable to kotlin. --- .../securesms/database/IdentityTable.java | 282 ------------------ .../securesms/database/IdentityTable.kt | 261 ++++++++++++++++ .../securesms/database/MessageTable.java | 4 +- .../PendingPniSignatureMessageTable.kt | 2 +- .../securesms/database/RecipientTable.kt | 2 +- .../signal/core/util/OptionalExtensions.kt | 4 + .../core/util/SQLiteDatabaseExtensions.kt | 35 ++- 7 files changed, 301 insertions(+), 289 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/IdentityTable.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/IdentityTable.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/IdentityTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/IdentityTable.java deleted file mode 100644 index 9f1c581b561..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/IdentityTable.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.database; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.greenrobot.eventbus.EventBus; -import org.signal.core.util.logging.Log; -import org.signal.libsignal.protocol.IdentityKey; -import org.signal.libsignal.protocol.InvalidKeyException; -import org.thoughtcrime.securesms.database.model.IdentityRecord; -import org.thoughtcrime.securesms.database.model.IdentityStoreRecord; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.storage.StorageSyncHelper; -import org.thoughtcrime.securesms.util.Base64; -import org.signal.core.util.CursorUtil; -import org.thoughtcrime.securesms.util.IdentityUtil; -import org.signal.core.util.SqlUtil; -import org.whispersystems.signalservice.api.push.ServiceId; -import org.whispersystems.signalservice.api.util.UuidUtil; - -import java.io.IOException; -import java.util.Optional; - -public class IdentityTable extends DatabaseTable { - - @SuppressWarnings("unused") - private static final String TAG = Log.tag(IdentityTable.class); - - static final String TABLE_NAME = "identities"; - private static final String ID = "_id"; - static final String ADDRESS = "address"; - static final String IDENTITY_KEY = "identity_key"; - private static final String FIRST_USE = "first_use"; - private static final String TIMESTAMP = "timestamp"; - static final String VERIFIED = "verified"; - private static final String NONBLOCKING_APPROVAL = "nonblocking_approval"; - - public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + - ADDRESS + " INTEGER UNIQUE, " + - IDENTITY_KEY + " TEXT, " + - FIRST_USE + " INTEGER DEFAULT 0, " + - TIMESTAMP + " INTEGER DEFAULT 0, " + - VERIFIED + " INTEGER DEFAULT 0, " + - NONBLOCKING_APPROVAL + " INTEGER DEFAULT 0);"; - - public enum VerifiedStatus { - DEFAULT, VERIFIED, UNVERIFIED; - - public int toInt() { - switch (this) { - case DEFAULT: return 0; - case VERIFIED: return 1; - case UNVERIFIED: return 2; - default: throw new AssertionError(); - } - } - - public static VerifiedStatus forState(int state) { - switch (state) { - case 0: return DEFAULT; - case 1: return VERIFIED; - case 2: return UNVERIFIED; - default: throw new AssertionError("No such state: " + state); - } - } - } - - IdentityTable(Context context, SignalDatabase databaseHelper) { - super(context, databaseHelper); - } - - public @Nullable IdentityStoreRecord getIdentityStoreRecord(@NonNull String addressName) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - String query = ADDRESS + " = ?"; - String[] args = SqlUtil.buildArgs(addressName); - - try (Cursor cursor = database.query(TABLE_NAME, null, query, args, null, null, null)) { - if (cursor.moveToFirst()) { - String serializedIdentity = CursorUtil.requireString(cursor, IDENTITY_KEY); - long timestamp = CursorUtil.requireLong(cursor, TIMESTAMP); - int verifiedStatus = CursorUtil.requireInt(cursor, VERIFIED); - boolean nonblockingApproval = CursorUtil.requireBoolean(cursor, NONBLOCKING_APPROVAL); - boolean firstUse = CursorUtil.requireBoolean(cursor, FIRST_USE); - - return new IdentityStoreRecord(addressName, - new IdentityKey(Base64.decode(serializedIdentity), 0), - VerifiedStatus.forState(verifiedStatus), - firstUse, - timestamp, - nonblockingApproval); - } else if (UuidUtil.isUuid(addressName)) { - Optional byServiceId = SignalDatabase.recipients().getByServiceId(ServiceId.parseOrThrow(addressName)); - - if (byServiceId.isPresent()) { - Recipient recipient = Recipient.resolved(byServiceId.get()); - - if (recipient.hasE164() && !UuidUtil.isUuid(recipient.requireE164())) { - Log.i(TAG, "Could not find identity for UUID. Attempting E164."); - return getIdentityStoreRecord(recipient.requireE164()); - } else { - Log.i(TAG, "Could not find identity for UUID, and our recipient doesn't have an E164."); - } - } else { - Log.i(TAG, "Could not find identity for UUID, and we don't have a recipient."); - } - } else { - Log.i(TAG, "Could not find identity for E164 either."); - } - } catch (InvalidKeyException | IOException e) { - throw new AssertionError(e); - } - - return null; - } - - public void saveIdentity(@NonNull String addressName, - @NonNull RecipientId recipientId, - IdentityKey identityKey, - VerifiedStatus verifiedStatus, - boolean firstUse, - long timestamp, - boolean nonBlockingApproval) - { - saveIdentityInternal(addressName, recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval); - SignalDatabase.recipients().markNeedsSync(recipientId); - StorageSyncHelper.scheduleSyncForDataChange(); - } - - public void setApproval(@NonNull String addressName, @NonNull RecipientId recipientId, boolean nonBlockingApproval) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - - ContentValues contentValues = new ContentValues(2); - contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval); - - database.update(TABLE_NAME, contentValues, ADDRESS + " = ?", SqlUtil.buildArgs(addressName)); - - SignalDatabase.recipients().markNeedsSync(recipientId); - StorageSyncHelper.scheduleSyncForDataChange(); - } - - public void setVerified(@NonNull String addressName, @NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - - String query = ADDRESS + " = ? AND " + IDENTITY_KEY + " = ?"; - String[] args = SqlUtil.buildArgs(addressName, Base64.encodeBytes(identityKey.serialize())); - - ContentValues contentValues = new ContentValues(1); - contentValues.put(VERIFIED, verifiedStatus.toInt()); - - int updated = database.update(TABLE_NAME, contentValues, query, args); - - if (updated > 0) { - Optional record = getIdentityRecord(addressName); - if (record.isPresent()) EventBus.getDefault().post(record.get()); - SignalDatabase.recipients().markNeedsSync(recipientId); - StorageSyncHelper.scheduleSyncForDataChange(); - } - } - - public void updateIdentityAfterSync(@NonNull String addressName, @NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) { - Optional existingRecord = getIdentityRecord(addressName); - - boolean hadEntry = existingRecord.isPresent(); - boolean keyMatches = hasMatchingKey(addressName, identityKey); - boolean statusMatches = keyMatches && hasMatchingStatus(addressName, identityKey, verifiedStatus); - - if (!keyMatches || !statusMatches) { - saveIdentityInternal(addressName, recipientId, identityKey, verifiedStatus, !hadEntry, System.currentTimeMillis(), true); - - Optional record = getIdentityRecord(addressName); - - if (record.isPresent()) { - EventBus.getDefault().post(record.get()); - } - - ApplicationDependencies.getProtocolStore().aci().identities().invalidate(addressName); - } - - if (hadEntry && !keyMatches) { - Log.w(TAG, "Updated identity key during storage sync for " + addressName + " | Existing: " + existingRecord.get().getIdentityKey().hashCode() + ", New: " + identityKey.hashCode()); - IdentityUtil.markIdentityUpdate(context, recipientId); - } - } - - public void delete(@NonNull String addressName) { - databaseHelper.getSignalWritableDatabase().delete(IdentityTable.TABLE_NAME, IdentityTable.ADDRESS + " = ?", SqlUtil.buildArgs(addressName)); - } - - private Optional getIdentityRecord(@NonNull String addressName) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - String query = ADDRESS + " = ?"; - String[] args = SqlUtil.buildArgs(addressName); - - try (Cursor cursor = database.query(TABLE_NAME, null, query, args, null, null, null)) { - if (cursor.moveToFirst()) { - return Optional.of(getIdentityRecord(cursor)); - } - } catch (InvalidKeyException | IOException e) { - throw new AssertionError(e); - } - - return Optional.empty(); - } - - private boolean hasMatchingKey(@NonNull String addressName, IdentityKey identityKey) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String query = ADDRESS + " = ? AND " + IDENTITY_KEY + " = ?"; - String[] args = SqlUtil.buildArgs(addressName, Base64.encodeBytes(identityKey.serialize())); - - try (Cursor cursor = db.query(TABLE_NAME, null, query, args, null, null, null)) { - return cursor != null && cursor.moveToFirst(); - } - } - - private boolean hasMatchingStatus(@NonNull String addressName, IdentityKey identityKey, VerifiedStatus verifiedStatus) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String query = ADDRESS + " = ? AND " + IDENTITY_KEY + " = ? AND " + VERIFIED + " = ?"; - String[] args = SqlUtil.buildArgs(addressName, Base64.encodeBytes(identityKey.serialize()), verifiedStatus.toInt()); - - try (Cursor cursor = db.query(TABLE_NAME, null, query, args, null, null, null)) { - return cursor != null && cursor.moveToFirst(); - } - } - - private static @NonNull IdentityRecord getIdentityRecord(@NonNull Cursor cursor) throws IOException, InvalidKeyException { - String addressName = CursorUtil.requireString(cursor, ADDRESS); - String serializedIdentity = CursorUtil.requireString(cursor, IDENTITY_KEY); - long timestamp = CursorUtil.requireLong(cursor, TIMESTAMP); - int verifiedStatus = CursorUtil.requireInt(cursor, VERIFIED); - boolean nonblockingApproval = CursorUtil.requireBoolean(cursor, NONBLOCKING_APPROVAL); - boolean firstUse = CursorUtil.requireBoolean(cursor, FIRST_USE); - IdentityKey identity = new IdentityKey(Base64.decode(serializedIdentity), 0); - - return new IdentityRecord(RecipientId.fromSidOrE164(addressName), identity, VerifiedStatus.forState(verifiedStatus), firstUse, timestamp, nonblockingApproval); - } - - private void saveIdentityInternal(@NonNull String addressName, - @NonNull RecipientId recipientId, - IdentityKey identityKey, - VerifiedStatus verifiedStatus, - boolean firstUse, - long timestamp, - boolean nonBlockingApproval) - { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - String identityKeyString = Base64.encodeBytes(identityKey.serialize()); - - ContentValues contentValues = new ContentValues(); - contentValues.put(ADDRESS, addressName); - contentValues.put(IDENTITY_KEY, identityKeyString); - contentValues.put(TIMESTAMP, timestamp); - contentValues.put(VERIFIED, verifiedStatus.toInt()); - contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval ? 1 : 0); - contentValues.put(FIRST_USE, firstUse ? 1 : 0); - - database.replace(TABLE_NAME, null, contentValues); - - EventBus.getDefault().post(new IdentityRecord(recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval)); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/IdentityTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/IdentityTable.kt new file mode 100644 index 00000000000..7b865f1e7cf --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/IdentityTable.kt @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2011 Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms.database + +import android.content.Context +import androidx.core.content.contentValuesOf +import org.greenrobot.eventbus.EventBus +import org.signal.core.util.delete +import org.signal.core.util.exists +import org.signal.core.util.firstOrNull +import org.signal.core.util.logging.Log +import org.signal.core.util.requireBoolean +import org.signal.core.util.requireInt +import org.signal.core.util.requireLong +import org.signal.core.util.requireNonNullString +import org.signal.core.util.select +import org.signal.core.util.toOptional +import org.signal.core.util.update +import org.signal.libsignal.protocol.IdentityKey +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients +import org.thoughtcrime.securesms.database.model.IdentityRecord +import org.thoughtcrime.securesms.database.model.IdentityStoreRecord +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.storage.StorageSyncHelper +import org.thoughtcrime.securesms.util.Base64 +import org.thoughtcrime.securesms.util.IdentityUtil +import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.api.util.UuidUtil +import java.lang.AssertionError +import java.util.Optional + +class IdentityTable internal constructor(context: Context?, databaseHelper: SignalDatabase?) : DatabaseTable(context, databaseHelper) { + + companion object { + private val TAG = Log.tag(IdentityTable::class.java) + const val TABLE_NAME = "identities" + private const val ID = "_id" + const val ADDRESS = "address" + const val IDENTITY_KEY = "identity_key" + private const val FIRST_USE = "first_use" + private const val TIMESTAMP = "timestamp" + const val VERIFIED = "verified" + private const val NONBLOCKING_APPROVAL = "nonblocking_approval" + const val CREATE_TABLE = """ + CREATE TABLE $TABLE_NAME ( + $ID INTEGER PRIMARY KEY AUTOINCREMENT, + $ADDRESS INTEGER UNIQUE, + $IDENTITY_KEY TEXT, + $FIRST_USE INTEGER DEFAULT 0, + $TIMESTAMP INTEGER DEFAULT 0, + $VERIFIED INTEGER DEFAULT 0, + $NONBLOCKING_APPROVAL INTEGER DEFAULT 0 + ) + """ + } + + fun getIdentityStoreRecord(addressName: String): IdentityStoreRecord? { + readableDatabase + .select() + .from(TABLE_NAME) + .where("$ADDRESS = ?", addressName) + .run() + .use { cursor -> + if (cursor.moveToFirst()) { + return IdentityStoreRecord( + addressName = addressName, + identityKey = IdentityKey(Base64.decode(cursor.requireNonNullString(IDENTITY_KEY)), 0), + verifiedStatus = VerifiedStatus.forState(cursor.requireInt(VERIFIED)), + firstUse = cursor.requireBoolean(FIRST_USE), + timestamp = cursor.requireLong(TIMESTAMP), + nonblockingApproval = cursor.requireBoolean(NONBLOCKING_APPROVAL) + ) + } else if (UuidUtil.isUuid(addressName)) { + val byServiceId = recipients.getByServiceId(ServiceId.parseOrThrow(addressName)) + if (byServiceId.isPresent) { + val recipient = Recipient.resolved(byServiceId.get()) + if (recipient.hasE164() && !UuidUtil.isUuid(recipient.requireE164())) { + Log.i(TAG, "Could not find identity for UUID. Attempting E164.") + return getIdentityStoreRecord(recipient.requireE164()) + } else { + Log.i(TAG, "Could not find identity for UUID, and our recipient doesn't have an E164.") + } + } else { + Log.i(TAG, "Could not find identity for UUID, and we don't have a recipient.") + } + } else { + Log.i(TAG, "Could not find identity for E164 either.") + } + } + + return null + } + + fun saveIdentity( + addressName: String, + recipientId: RecipientId, + identityKey: IdentityKey, + verifiedStatus: VerifiedStatus, + firstUse: Boolean, + timestamp: Long, + nonBlockingApproval: Boolean + ) { + saveIdentityInternal(addressName, recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval) + recipients.markNeedsSync(recipientId) + StorageSyncHelper.scheduleSyncForDataChange() + } + + fun setApproval(addressName: String, recipientId: RecipientId, nonBlockingApproval: Boolean) { + val updated = writableDatabase + .update(TABLE_NAME) + .values(NONBLOCKING_APPROVAL to nonBlockingApproval) + .where("$ADDRESS = ?", addressName) + .run() + + if (updated > 0) { + recipients.markNeedsSync(recipientId) + StorageSyncHelper.scheduleSyncForDataChange() + } + } + + fun setVerified(addressName: String, recipientId: RecipientId, identityKey: IdentityKey, verifiedStatus: VerifiedStatus) { + val updated = writableDatabase + .update(TABLE_NAME) + .values(VERIFIED to verifiedStatus.toInt()) + .where("$ADDRESS = ? AND $IDENTITY_KEY = ?", addressName, Base64.encodeBytes(identityKey.serialize())) + .run() + + if (updated > 0) { + val record = getIdentityRecord(addressName) + if (record.isPresent) { + EventBus.getDefault().post(record.get()) + } + recipients.markNeedsSync(recipientId) + StorageSyncHelper.scheduleSyncForDataChange() + } + } + + fun updateIdentityAfterSync(addressName: String, recipientId: RecipientId, identityKey: IdentityKey, verifiedStatus: VerifiedStatus) { + val existingRecord = getIdentityRecord(addressName) + val hadEntry = existingRecord.isPresent + val keyMatches = hasMatchingKey(addressName, identityKey) + val statusMatches = keyMatches && hasMatchingStatus(addressName, identityKey, verifiedStatus) + + if (!keyMatches || !statusMatches) { + saveIdentityInternal(addressName, recipientId, identityKey, verifiedStatus, !hadEntry, System.currentTimeMillis(), nonBlockingApproval = true) + + val record = getIdentityRecord(addressName) + if (record.isPresent) { + EventBus.getDefault().post(record.get()) + } + + ApplicationDependencies.getProtocolStore().aci().identities().invalidate(addressName) + } + + if (hadEntry && !keyMatches) { + Log.w(TAG, "Updated identity key during storage sync for " + addressName + " | Existing: " + existingRecord.get().identityKey.hashCode() + ", New: " + identityKey.hashCode()) + IdentityUtil.markIdentityUpdate(context, recipientId) + } + } + + fun delete(addressName: String) { + writableDatabase + .delete(TABLE_NAME) + .where("$ADDRESS = ?", addressName) + .run() + } + + private fun getIdentityRecord(addressName: String): Optional { + return readableDatabase + .select() + .from(TABLE_NAME) + .where("$ADDRESS = ?", addressName) + .run() + .firstOrNull { cursor -> + IdentityRecord( + recipientId = RecipientId.fromSidOrE164(cursor.requireNonNullString(ADDRESS)), + identityKey = IdentityKey(Base64.decode(cursor.requireNonNullString(IDENTITY_KEY)), 0), + verifiedStatus = VerifiedStatus.forState(cursor.requireInt(VERIFIED)), + firstUse = cursor.requireBoolean(FIRST_USE), + timestamp = cursor.requireLong(TIMESTAMP), + nonblockingApproval = cursor.requireBoolean(NONBLOCKING_APPROVAL) + ) + } + .toOptional() + } + + private fun hasMatchingKey(addressName: String, identityKey: IdentityKey): Boolean { + return readableDatabase + .exists(TABLE_NAME) + .where("$ADDRESS = ? AND $IDENTITY_KEY = ?", addressName, Base64.encodeBytes(identityKey.serialize())) + .run() + } + + private fun hasMatchingStatus(addressName: String, identityKey: IdentityKey, verifiedStatus: VerifiedStatus): Boolean { + return readableDatabase + .exists(TABLE_NAME) + .where("$ADDRESS = ? AND $IDENTITY_KEY = ? AND $VERIFIED = ?", addressName, Base64.encodeBytes(identityKey.serialize()), verifiedStatus.toInt()) + .run() + } + + private fun saveIdentityInternal( + addressName: String, + recipientId: RecipientId, + identityKey: IdentityKey, + verifiedStatus: VerifiedStatus, + firstUse: Boolean, + timestamp: Long, + nonBlockingApproval: Boolean + ) { + val contentValues = contentValuesOf( + ADDRESS to addressName, + IDENTITY_KEY to Base64.encodeBytes(identityKey.serialize()), + TIMESTAMP to timestamp, + VERIFIED to verifiedStatus.toInt(), + NONBLOCKING_APPROVAL to if (nonBlockingApproval) 1 else 0, + FIRST_USE to if (firstUse) 1 else 0 + ) + writableDatabase.replace(TABLE_NAME, null, contentValues) + EventBus.getDefault().post(IdentityRecord(recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval)) + } + + enum class VerifiedStatus { + DEFAULT, VERIFIED, UNVERIFIED; + + fun toInt(): Int { + return when (this) { + DEFAULT -> 0 + VERIFIED -> 1 + UNVERIFIED -> 2 + } + } + + companion object { + @JvmStatic + fun forState(state: Int): VerifiedStatus { + return when (state) { + 0 -> DEFAULT + 1 -> VERIFIED + 2 -> UNVERIFIED + else -> throw AssertionError("No such state: $state") + } + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.java index 46310a1a018..54ab76b6fd0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.java @@ -268,7 +268,9 @@ public int getInsecureMessageCount() { } public boolean hasSmsExportMessage(long threadId) { - return SQLiteDatabaseExtensionsKt.exists(getReadableDatabase(), getTableName(), THREAD_ID_WHERE + " AND " + getTypeField() + " = ?", threadId, Types.SMS_EXPORT_TYPE); + return SQLiteDatabaseExtensionsKt.exists(getReadableDatabase(), getTableName()) + .where(THREAD_ID_WHERE + " AND " + getTypeField() + " = ?", threadId, Types.SMS_EXPORT_TYPE) + .run(); } final int getSecureMessageCountForInsights() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/PendingPniSignatureMessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/PendingPniSignatureMessageTable.kt index 685db6044b5..7011a22467a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/PendingPniSignatureMessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/PendingPniSignatureMessageTable.kt @@ -75,7 +75,7 @@ class PendingPniSignatureMessageTable(context: Context, databaseHelper: SignalDa return@withinTransaction } - val stillPending: Boolean = db.exists(TABLE_NAME, "$RECIPIENT_ID = ? AND $SENT_TIMESTAMP = ?", recipientId, sentTimestamps) + val stillPending: Boolean = db.exists(TABLE_NAME).where("$RECIPIENT_ID = ? AND $SENT_TIMESTAMP = ?", recipientId, sentTimestamps).run() if (!stillPending) { Log.i(TAG, "All devices for ($recipientId, $sentTimestamps) have acked the PNI signature message. Clearing flag and removing any other pending receipts.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index d876146485c..8f8e96986e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -419,7 +419,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da } fun isAssociated(serviceId: ServiceId, pni: PNI): Boolean { - return readableDatabase.exists(TABLE_NAME, "$SERVICE_ID = ? AND $PNI_COLUMN = ?", serviceId.toString(), pni.toString()) + return readableDatabase.exists(TABLE_NAME).where("$SERVICE_ID = ? AND $PNI_COLUMN = ?", serviceId.toString(), pni.toString()).run() } @JvmOverloads diff --git a/core-util/src/main/java/org/signal/core/util/OptionalExtensions.kt b/core-util/src/main/java/org/signal/core/util/OptionalExtensions.kt index e09de4346c5..4f4feadba75 100644 --- a/core-util/src/main/java/org/signal/core/util/OptionalExtensions.kt +++ b/core-util/src/main/java/org/signal/core/util/OptionalExtensions.kt @@ -12,4 +12,8 @@ fun Optional.or(other: Optional): Optional { fun Optional.isAbsent(): Boolean { return !isPresent +} + +fun E?.toOptional(): Optional { + return Optional.ofNullable(this) } \ No newline at end of file diff --git a/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt b/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt index c7140e1b772..fdcd1617489 100644 --- a/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt +++ b/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt @@ -38,10 +38,8 @@ fun SupportSQLiteDatabase.getTableRowCount(table: String): Int { /** * Checks if a row exists that matches the query. */ -fun SupportSQLiteDatabase.exists(table: String, query: String, vararg args: Any): Boolean { - return this.query("SELECT EXISTS(SELECT 1 FROM $table WHERE $query)", SqlUtil.buildArgs(*args)).use { cursor -> - cursor.moveToFirst() && cursor.getInt(0) == 1 - } +fun SupportSQLiteDatabase.exists(table: String): ExistsBuilderPart1 { + return ExistsBuilderPart1(this, table) } /** @@ -259,3 +257,32 @@ class DeleteBuilderPart2( return db.delete(tableName, where, whereArgs) } } + +class ExistsBuilderPart1( + private val db: SupportSQLiteDatabase, + private val tableName: String +) { + + fun where(@Language("sql") where: String, vararg whereArgs: Any): ExistsBuilderPart2 { + return ExistsBuilderPart2(db, tableName, where, SqlUtil.buildArgs(*whereArgs)) + } + + fun run(): Boolean { + return db.query("SELECT EXISTS(SELECT 1 FROM $tableName)", null).use { cursor -> + cursor.moveToFirst() && cursor.getInt(0) == 1 + } + } +} + +class ExistsBuilderPart2( + private val db: SupportSQLiteDatabase, + private val tableName: String, + private val where: String, + private val whereArgs: Array +) { + fun run(): Boolean { + return db.query("SELECT EXISTS(SELECT 1 FROM $tableName WHERE $where)", SqlUtil.buildArgs(*whereArgs)).use { cursor -> + cursor.moveToFirst() && cursor.getInt(0) == 1 + } + } +}