Convert IdentityTable to kotlin.

main
Greyson Parrelli 2022-12-08 09:05:32 -05:00 zatwierdzone przez Alex Hart
rodzic 380b377ed8
commit 69003dfbe2
7 zmienionych plików z 301 dodań i 289 usunięć

Wyświetl plik

@ -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 <http://www.gnu.org/licenses/>.
*/
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<RecipientId> 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<IdentityRecord> 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<IdentityRecord> 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<IdentityRecord> 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<IdentityRecord> 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));
}
}

Wyświetl plik

@ -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 <http://www.gnu.org/licenses/>.
*/
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<IdentityRecord> {
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")
}
}
}
}
}

Wyświetl plik

@ -268,7 +268,9 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn
}
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() {

Wyświetl plik

@ -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.")

Wyświetl plik

@ -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

Wyświetl plik

@ -12,4 +12,8 @@ fun <E> Optional<E>.or(other: Optional<E>): Optional<E> {
fun <E> Optional<E>.isAbsent(): Boolean {
return !isPresent
}
fun <E : Any> E?.toOptional(): Optional<E> {
return Optional.ofNullable(this)
}

Wyświetl plik

@ -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<String>
) {
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
}
}
}