mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 20:18:36 +00:00
Convert IdentityTable to kotlin.
This commit is contained in:
committed by
Alex Hart
parent
380b377ed8
commit
69003dfbe2
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -268,7 +268,9 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasSmsExportMessage(long threadId) {
|
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() {
|
final int getSecureMessageCountForInsights() {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class PendingPniSignatureMessageTable(context: Context, databaseHelper: SignalDa
|
|||||||
return@withinTransaction
|
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) {
|
if (!stillPending) {
|
||||||
Log.i(TAG, "All devices for ($recipientId, $sentTimestamps) have acked the PNI signature message. Clearing flag and removing any other pending receipts.")
|
Log.i(TAG, "All devices for ($recipientId, $sentTimestamps) have acked the PNI signature message. Clearing flag and removing any other pending receipts.")
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isAssociated(serviceId: ServiceId, pni: PNI): Boolean {
|
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
|
@JvmOverloads
|
||||||
|
|||||||
@@ -12,4 +12,8 @@ fun <E> Optional<E>.or(other: Optional<E>): Optional<E> {
|
|||||||
|
|
||||||
fun <E> Optional<E>.isAbsent(): Boolean {
|
fun <E> Optional<E>.isAbsent(): Boolean {
|
||||||
return !isPresent
|
return !isPresent
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <E : Any> E?.toOptional(): Optional<E> {
|
||||||
|
return Optional.ofNullable(this)
|
||||||
}
|
}
|
||||||
@@ -38,10 +38,8 @@ fun SupportSQLiteDatabase.getTableRowCount(table: String): Int {
|
|||||||
/**
|
/**
|
||||||
* Checks if a row exists that matches the query.
|
* Checks if a row exists that matches the query.
|
||||||
*/
|
*/
|
||||||
fun SupportSQLiteDatabase.exists(table: String, query: String, vararg args: Any): Boolean {
|
fun SupportSQLiteDatabase.exists(table: String): ExistsBuilderPart1 {
|
||||||
return this.query("SELECT EXISTS(SELECT 1 FROM $table WHERE $query)", SqlUtil.buildArgs(*args)).use { cursor ->
|
return ExistsBuilderPart1(this, table)
|
||||||
cursor.moveToFirst() && cursor.getInt(0) == 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -259,3 +257,32 @@ class DeleteBuilderPart2(
|
|||||||
return db.delete(tableName, where, whereArgs)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user