kopia lustrzana https://github.com/ryukoposting/Signal-Android
974 wiersze
40 KiB
Java
974 wiersze
40 KiB
Java
package org.thoughtcrime.securesms.database;
|
|
|
|
import android.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.database.Cursor;
|
|
import android.text.TextUtils;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import com.google.android.mms.pdu_alt.NotificationInd;
|
|
|
|
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
|
|
|
import org.signal.core.util.CursorExtensionsKt;
|
|
import org.signal.core.util.CursorUtil;
|
|
import org.signal.core.util.SQLiteDatabaseExtensionsKt;
|
|
import org.signal.core.util.SqlUtil;
|
|
import org.signal.core.util.logging.Log;
|
|
import org.signal.libsignal.protocol.IdentityKey;
|
|
import org.signal.libsignal.protocol.util.Pair;
|
|
import org.thoughtcrime.securesms.database.documents.Document;
|
|
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
|
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet;
|
|
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
|
import org.thoughtcrime.securesms.database.model.MessageExportStatus;
|
|
import org.thoughtcrime.securesms.database.model.MessageId;
|
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|
import org.thoughtcrime.securesms.database.model.ParentStoryId;
|
|
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
|
import org.thoughtcrime.securesms.database.model.StoryResult;
|
|
import org.thoughtcrime.securesms.database.model.StoryType;
|
|
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
|
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState;
|
|
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent;
|
|
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
|
import org.thoughtcrime.securesms.insights.InsightsConstants;
|
|
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
|
import org.thoughtcrime.securesms.mms.MmsException;
|
|
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
|
import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo;
|
|
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
|
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
|
import org.thoughtcrime.securesms.util.JsonUtils;
|
|
import org.thoughtcrime.securesms.util.Util;
|
|
|
|
import java.io.Closeable;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Iterator;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
|
|
public abstract class MessageDatabase extends Database implements MmsSmsColumns, RecipientIdDatabaseReference, ThreadIdDatabaseReference {
|
|
|
|
private static final String TAG = Log.tag(MessageDatabase.class);
|
|
|
|
protected static final String THREAD_ID_WHERE = THREAD_ID + " = ?";
|
|
protected static final String[] THREAD_ID_PROJECTION = new String[] { THREAD_ID };
|
|
|
|
public MessageDatabase(Context context, SignalDatabase databaseHelper) {
|
|
super(context, databaseHelper);
|
|
}
|
|
|
|
protected abstract String getTableName();
|
|
protected abstract String getTypeField();
|
|
protected abstract String getDateSentColumnName();
|
|
protected abstract String getDateReceivedColumnName();
|
|
|
|
public abstract @Nullable RecipientId getOldestGroupUpdateSender(long threadId, long minimumDateReceived);
|
|
public abstract long getLatestGroupQuitTimestamp(long threadId, long quitTimeBarrier);
|
|
public abstract boolean isGroupQuitMessage(long messageId);
|
|
public abstract @Nullable Pair<RecipientId, Long> getOldestUnreadMentionDetails(long threadId);
|
|
public abstract int getUnreadMentionCount(long threadId);
|
|
public abstract long getThreadIdForMessage(long id);
|
|
public abstract int getMessageCountForThread(long threadId);
|
|
public abstract int getMessageCountForThread(long threadId, long beforeTime);
|
|
public abstract boolean hasMeaningfulMessage(long threadId);
|
|
public abstract int getIncomingMeaningfulMessageCountSince(long threadId, long afterTime);
|
|
public abstract Optional<MmsNotificationInfo> getNotification(long messageId);
|
|
|
|
public abstract Cursor getExpirationStartedMessages();
|
|
public abstract SmsMessageRecord getSmsMessage(long messageId) throws NoSuchMessageException;
|
|
public abstract Reader getMessages(Collection<Long> messageIds);
|
|
public abstract Cursor getMessageCursor(long messageId);
|
|
public abstract OutgoingMediaMessage getOutgoingMessage(long messageId) throws MmsException, NoSuchMessageException;
|
|
public abstract MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException;
|
|
public abstract @Nullable MessageRecord getMessageRecordOrNull(long messageId);
|
|
public abstract boolean hasReceivedAnyCallsSince(long threadId, long timestamp);
|
|
public abstract @Nullable ViewOnceExpirationInfo getNearestExpiringViewOnceMessage();
|
|
public abstract boolean isSent(long messageId);
|
|
public abstract List<MessageRecord> getProfileChangeDetailsRecords(long threadId, long afterTimestamp);
|
|
public abstract Set<Long> getAllRateLimitedMessageIds();
|
|
public abstract Cursor getUnexportedInsecureMessages(int limit);
|
|
public abstract long getUnexportedInsecureMessagesEstimatedSize();
|
|
public abstract void deleteExportedMessages();
|
|
|
|
public abstract void markExpireStarted(long messageId);
|
|
public abstract void markExpireStarted(long messageId, long startTime);
|
|
public abstract void markExpireStarted(Collection<Long> messageId, long startTime);
|
|
|
|
public abstract void markAsEndSession(long id);
|
|
public abstract void markAsInvalidVersionKeyExchange(long id);
|
|
public abstract void markAsSecure(long id);
|
|
public abstract void markAsInsecure(long id);
|
|
public abstract void markAsPush(long id);
|
|
public abstract void markAsForcedSms(long id);
|
|
public abstract void markAsRateLimited(long id);
|
|
public abstract void clearRateLimitStatus(Collection<Long> ids);
|
|
public abstract void markAsDecryptFailed(long id);
|
|
public abstract void markAsNoSession(long id);
|
|
public abstract void markAsUnsupportedProtocolVersion(long id);
|
|
public abstract void markAsInvalidMessage(long id);
|
|
public abstract void markAsLegacyVersion(long id);
|
|
public abstract void markAsOutbox(long id);
|
|
public abstract void markAsPendingInsecureSmsFallback(long id);
|
|
public abstract void markAsSent(long messageId, boolean secure);
|
|
public abstract void markUnidentified(long messageId, boolean unidentified);
|
|
public abstract void markAsSentFailed(long id);
|
|
public abstract void markAsSending(long messageId);
|
|
public abstract void markAsRemoteDelete(long messageId);
|
|
public abstract void markAsMissedCall(long id, boolean isVideoOffer);
|
|
public abstract void markAsNotified(long id);
|
|
public abstract void markSmsStatus(long id, int status);
|
|
public abstract void markDownloadState(long messageId, long state);
|
|
public abstract void markIncomingNotificationReceived(long threadId);
|
|
public abstract void markGiftRedemptionCompleted(long messageId);
|
|
public abstract void markGiftRedemptionStarted(long messageId);
|
|
public abstract void markGiftRedemptionFailed(long messageId);
|
|
|
|
public abstract Set<MessageUpdate> incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, @NonNull MessageQualifier messageType);
|
|
|
|
public abstract List<MarkedMessageInfo> setEntireThreadRead(long threadId);
|
|
public abstract List<MarkedMessageInfo> setMessagesReadSince(long threadId, long timestamp);
|
|
public abstract List<MarkedMessageInfo> setAllMessagesRead();
|
|
public abstract InsertResult updateBundleMessageBody(long messageId, String body);
|
|
public abstract @NonNull List<MarkedMessageInfo> getViewedIncomingMessages(long threadId);
|
|
public abstract @Nullable MarkedMessageInfo setIncomingMessageViewed(long messageId);
|
|
public abstract @NonNull List<MarkedMessageInfo> setIncomingMessagesViewed(@NonNull List<Long> messageIds);
|
|
public abstract @NonNull List<MarkedMessageInfo> setOutgoingGiftsRevealed(@NonNull List<Long> messageIds);
|
|
|
|
public abstract void addFailures(long messageId, List<NetworkFailure> failure);
|
|
public abstract void setNetworkFailures(long messageId, Set<NetworkFailure> failures);
|
|
|
|
public abstract @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer);
|
|
public abstract @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer);
|
|
public abstract @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer);
|
|
public abstract void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId,
|
|
@NonNull RecipientId sender,
|
|
long timestamp,
|
|
@Nullable String peekGroupCallEraId,
|
|
@NonNull Collection<UUID> peekJoinedUuids,
|
|
boolean isCallFull);
|
|
public abstract void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId,
|
|
@NonNull RecipientId sender,
|
|
long timestamp,
|
|
@Nullable String messageGroupCallEraId);
|
|
public abstract boolean updatePreviousGroupCall(long threadId, @Nullable String peekGroupCallEraId, @NonNull Collection<UUID> peekJoinedUuids, boolean isCallFull);
|
|
|
|
public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type);
|
|
public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message);
|
|
public abstract Optional<InsertResult> insertMessageInbox(IncomingMediaMessage retrieved, String contentLocation, long threadId) throws MmsException;
|
|
public abstract Pair<Long, Long> insertMessageInbox(@NonNull NotificationInd notification, int subscriptionId);
|
|
public abstract Optional<InsertResult> insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId) throws MmsException;
|
|
public abstract @NonNull InsertResult insertChatSessionRefreshedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp);
|
|
public abstract void insertBadDecryptMessage(@NonNull RecipientId recipientId, int senderDevice, long sentTimestamp, long receivedTimestamp, long threadId);
|
|
public abstract long insertMessageOutbox(long threadId, OutgoingTextMessage message, boolean forceSms, long date, InsertListener insertListener);
|
|
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
|
|
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, int defaultReceiptStatus, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
|
|
public abstract void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName);
|
|
public abstract void insertGroupV1MigrationEvents(@NonNull RecipientId recipientId, long threadId, @NonNull GroupMigrationMembershipChange membershipChange);
|
|
public abstract void insertNumberChangeMessages(@NonNull RecipientId recipientId);
|
|
public abstract void insertBoostRequestMessage(@NonNull RecipientId recipientId, long threadId);
|
|
public abstract void insertThreadMergeEvent(@NonNull RecipientId recipientId, long threadId, @NonNull ThreadMergeEvent event);
|
|
public abstract void insertSmsExportMessage(@NonNull RecipientId recipientId, long threadId);
|
|
|
|
public abstract boolean deleteMessage(long messageId);
|
|
abstract void deleteThread(long threadId);
|
|
abstract int deleteMessagesInThreadBeforeDate(long threadId, long date);
|
|
abstract void deleteThreads(@NonNull Set<Long> threadIds);
|
|
abstract void deleteAllThreads();
|
|
abstract void deleteAbandonedMessages();
|
|
public abstract void deleteRemotelyDeletedStory(long messageId);
|
|
|
|
public abstract List<MessageRecord> getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit);
|
|
|
|
public abstract SQLiteDatabase beginTransaction();
|
|
public abstract void endTransaction(SQLiteDatabase database);
|
|
public abstract void setTransactionSuccessful();
|
|
public abstract void endTransaction();
|
|
public abstract SQLiteStatement createInsertStatement(SQLiteDatabase database);
|
|
|
|
public abstract void ensureMigration();
|
|
|
|
public abstract boolean isStory(long messageId);
|
|
public abstract @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId);
|
|
public abstract @NonNull Reader getAllOutgoingStories(boolean reverse, int limit);
|
|
public abstract @NonNull Reader getAllOutgoingStoriesAt(long sentTimestamp);
|
|
public abstract @NonNull List<MarkedMessageInfo> markAllIncomingStoriesRead();
|
|
public abstract @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds(boolean isOutgoingOnly);
|
|
|
|
public abstract void markOnboardingStoryRead();
|
|
|
|
public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId, int limit);
|
|
public abstract @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException;
|
|
public abstract int getNumberOfStoryReplies(long parentStoryId);
|
|
public abstract @NonNull List<RecipientId> getUnreadStoryThreadRecipientIds();
|
|
public abstract boolean containsStories(long threadId);
|
|
public abstract boolean hasSelfReplyInStory(long parentStoryId);
|
|
public abstract boolean hasGroupReplyOrReactionInStory(long parentStoryId);
|
|
public abstract @NonNull Cursor getStoryReplies(long parentStoryId);
|
|
public abstract @Nullable Long getOldestStorySendTimestamp(boolean hasSeenReleaseChannelStories);
|
|
public abstract int deleteStoriesOlderThan(long timestamp, boolean hasSeenReleaseChannelStories);
|
|
public abstract @NonNull MessageDatabase.Reader getUnreadStories(@NonNull RecipientId recipientId, int limit);
|
|
public abstract @Nullable ParentStoryId.GroupReply getParentStoryIdForGroupReply(long messageId);
|
|
public abstract void deleteGroupStoryReplies(long parentStoryId);
|
|
public abstract boolean isOutgoingStoryAlreadyInDatabase(@NonNull RecipientId recipientId, long sentTimestamp);
|
|
public abstract @NonNull List<MarkedMessageInfo> setGroupStoryMessagesReadSince(long threadId, long groupStoryId, long sinceTimestamp);
|
|
public abstract @NonNull List<StoryType> getStoryTypes(@NonNull List<MessageId> messageIds);
|
|
|
|
public abstract @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId);
|
|
public abstract void updateViewedStories(@NonNull Set<SyncMessageId> syncMessageIds);
|
|
|
|
final @NonNull String getOutgoingTypeClause() {
|
|
List<String> segments = new ArrayList<>(Types.OUTGOING_MESSAGE_TYPES.length);
|
|
for (long outgoingMessageType : Types.OUTGOING_MESSAGE_TYPES) {
|
|
segments.add("(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + " = " + outgoingMessageType + ")");
|
|
}
|
|
|
|
return Util.join(segments, " OR ");
|
|
}
|
|
|
|
final int getInsecureMessagesSentForThread(long threadId) {
|
|
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
|
String[] projection = new String[]{"COUNT(*)"};
|
|
String query = THREAD_ID + " = ? AND " + getOutgoingInsecureMessageClause() + " AND " + getDateSentColumnName() + " > ?";
|
|
String[] args = new String[]{String.valueOf(threadId), String.valueOf(System.currentTimeMillis() - InsightsConstants.PERIOD_IN_MILLIS)};
|
|
|
|
try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null, null)) {
|
|
if (cursor != null && cursor.moveToFirst()) {
|
|
return cursor.getInt(0);
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
final int getInsecureMessageCountForInsights() {
|
|
return getMessageCountForRecipientsAndType(getOutgoingInsecureMessageClause());
|
|
}
|
|
|
|
public int getInsecureMessageCount() {
|
|
try (Cursor cursor = getReadableDatabase().query(getTableName(), SqlUtil.COUNT, getInsecureMessageClause(), null, null, null, null)) {
|
|
if (cursor.moveToFirst()) {
|
|
return cursor.getInt(0);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public boolean hasSmsExportMessage(long threadId) {
|
|
return SQLiteDatabaseExtensionsKt.exists(getReadableDatabase(), getTableName(), THREAD_ID_WHERE + " AND " + getTypeField() + " = ?", threadId, Types.SMS_EXPORT_TYPE);
|
|
}
|
|
|
|
final int getSecureMessageCountForInsights() {
|
|
return getMessageCountForRecipientsAndType(getOutgoingSecureMessageClause());
|
|
}
|
|
|
|
final int getSecureMessageCount(long threadId) {
|
|
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
|
String[] projection = new String[] {"COUNT(*)"};
|
|
String query = getSecureMessageClause() + "AND " + MmsSmsColumns.THREAD_ID + " = ?";
|
|
String[] args = new String[]{String.valueOf(threadId)};
|
|
|
|
try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null, null)) {
|
|
if (cursor != null && cursor.moveToFirst()) {
|
|
return cursor.getInt(0);
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
final int getOutgoingSecureMessageCount(long threadId) {
|
|
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
|
String[] projection = new String[] {"COUNT(*)"};
|
|
String query = getOutgoingSecureMessageClause() +
|
|
"AND " + MmsSmsColumns.THREAD_ID + " = ? " +
|
|
"AND (" + getTypeField() + " & " + Types.GROUP_LEAVE_BIT + " = 0 OR " + getTypeField() + " & " + Types.GROUP_V2_BIT + " = " + Types.GROUP_V2_BIT + ")";
|
|
String[] args = new String[]{String.valueOf(threadId)};
|
|
|
|
try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null, null)) {
|
|
if (cursor != null && cursor.moveToFirst()) {
|
|
return cursor.getInt(0);
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles a synchronized read message.
|
|
* @param messageId An id representing the author-timestamp pair of the message that was read on a linked device. Note that the author could be self when
|
|
* syncing read receipts for reactions.
|
|
*/
|
|
final @NonNull MmsSmsDatabase.TimestampReadResult setTimestampReadFromSyncMessage(SyncMessageId messageId, long proposedExpireStarted, @NonNull Map<Long, Long> threadToLatestRead) {
|
|
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
|
|
List<Pair<Long, Long>> expiring = new LinkedList<>();
|
|
String[] projection = new String[] { ID, THREAD_ID, EXPIRES_IN, EXPIRE_STARTED };
|
|
String query = getDateSentColumnName() + " = ? AND (" + RECIPIENT_ID + " = ? OR (" + RECIPIENT_ID + " = ? AND " + getOutgoingTypeClause() + "))";
|
|
String[] args = SqlUtil.buildArgs(messageId.getTimetamp(), messageId.getRecipientId(), Recipient.self().getId());
|
|
List<Long> threads = new LinkedList<>();
|
|
|
|
try (Cursor cursor = database.query(getTableName(), projection, query, args, null, null, null)) {
|
|
while (cursor.moveToNext()) {
|
|
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
|
|
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID));
|
|
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN));
|
|
long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRE_STARTED));
|
|
|
|
expireStarted = expireStarted > 0 ? Math.min(proposedExpireStarted, expireStarted) : proposedExpireStarted;
|
|
|
|
ContentValues values = new ContentValues();
|
|
values.put(READ, 1);
|
|
values.put(REACTIONS_UNREAD, 0);
|
|
values.put(REACTIONS_LAST_SEEN, System.currentTimeMillis());
|
|
|
|
if (expiresIn > 0) {
|
|
values.put(EXPIRE_STARTED, expireStarted);
|
|
expiring.add(new Pair<>(id, expiresIn));
|
|
}
|
|
|
|
database.update(getTableName(), values, ID_WHERE, SqlUtil.buildArgs(id));
|
|
|
|
threads.add(threadId);
|
|
|
|
Long latest = threadToLatestRead.get(threadId);
|
|
threadToLatestRead.put(threadId, (latest != null) ? Math.max(latest, messageId.getTimetamp()) : messageId.getTimetamp());
|
|
}
|
|
}
|
|
|
|
return new MmsSmsDatabase.TimestampReadResult(expiring, threads);
|
|
}
|
|
|
|
private int getMessageCountForRecipientsAndType(String typeClause) {
|
|
|
|
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
|
String[] projection = new String[] {"COUNT(*)"};
|
|
String query = typeClause + " AND " + getDateSentColumnName() + " > ?";
|
|
String[] args = new String[]{String.valueOf(System.currentTimeMillis() - InsightsConstants.PERIOD_IN_MILLIS)};
|
|
|
|
try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null, null)) {
|
|
if (cursor != null && cursor.moveToFirst()) {
|
|
return cursor.getInt(0);
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
private String getOutgoingInsecureMessageClause() {
|
|
return "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND NOT (" + getTypeField() + " & " + Types.SECURE_MESSAGE_BIT + ")";
|
|
}
|
|
|
|
private String getOutgoingSecureMessageClause() {
|
|
return "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND (" + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")";
|
|
}
|
|
|
|
private String getSecureMessageClause() {
|
|
String isSent = "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE;
|
|
String isReceived = "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE;
|
|
String isSecure = "(" + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")";
|
|
|
|
return String.format(Locale.ENGLISH, "(%s OR %s) AND %s", isSent, isReceived, isSecure);
|
|
}
|
|
|
|
protected String getInsecureMessageClause() {
|
|
return getInsecureMessageClause(-1);
|
|
}
|
|
|
|
protected String getInsecureMessageClause(long threadId) {
|
|
String isSent = "(" + getTableName() + "." + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE;
|
|
String isReceived = "(" + getTableName() + "." + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE;
|
|
String isSecure = "(" + getTableName() + "." + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")";
|
|
String isNotSecure = "(" + getTableName() + "." + getTypeField() + " <= " + (Types.BASE_TYPE_MASK | Types.MESSAGE_ATTRIBUTE_MASK) + ")";
|
|
|
|
String whereClause = String.format(Locale.ENGLISH, "(%s OR %s) AND NOT %s AND %s", isSent, isReceived, isSecure, isNotSecure);
|
|
|
|
if (threadId != -1) {
|
|
whereClause += " AND " + getTableName() + "." + THREAD_ID + " = " + threadId;
|
|
}
|
|
|
|
return whereClause;
|
|
}
|
|
|
|
public int getUnexportedInsecureMessagesCount() {
|
|
return getUnexportedInsecureMessagesCount(-1);
|
|
}
|
|
|
|
public int getUnexportedInsecureMessagesCount(long threadId) {
|
|
try (Cursor cursor = getWritableDatabase().query(getTableName(), SqlUtil.COUNT, getInsecureMessageClause(threadId) + " AND " + EXPORTED + " < ?", SqlUtil.buildArgs(MessageExportStatus.EXPORTED), null, null, null)) {
|
|
if (cursor.moveToFirst()) {
|
|
return cursor.getInt(0);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Resets the exported state and exported flag so messages can be re-exported.
|
|
*/
|
|
public void clearExportState() {
|
|
ContentValues values = new ContentValues(2);
|
|
values.putNull(EXPORT_STATE);
|
|
values.put(EXPORTED, MessageExportStatus.UNEXPORTED.serialize());
|
|
|
|
SQLiteDatabaseExtensionsKt.update(getWritableDatabase(), getTableName())
|
|
.values(values)
|
|
.where(EXPORT_STATE + " IS NOT NULL OR " + EXPORTED + " != ?", MessageExportStatus.UNEXPORTED)
|
|
.run();
|
|
}
|
|
|
|
/**
|
|
* Reset the exported status (not state) to the default for clearing errors.
|
|
*/
|
|
public void clearInsecureMessageExportedErrorStatus() {
|
|
ContentValues values = new ContentValues(1);
|
|
values.put(EXPORTED, MessageExportStatus.UNEXPORTED.getCode());
|
|
|
|
SQLiteDatabaseExtensionsKt.update(getWritableDatabase(), getTableName())
|
|
.values(values)
|
|
.where(EXPORTED + " < ?", MessageExportStatus.UNEXPORTED)
|
|
.run();
|
|
}
|
|
|
|
public void setReactionsSeen(long threadId, long sinceTimestamp) {
|
|
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
|
|
ContentValues values = new ContentValues();
|
|
String whereClause = THREAD_ID + " = ? AND " + REACTIONS_UNREAD + " = ?";
|
|
String[] whereArgs = new String[]{String.valueOf(threadId), "1"};
|
|
|
|
if (sinceTimestamp > -1) {
|
|
whereClause += " AND " + getDateReceivedColumnName() + " <= " + sinceTimestamp;
|
|
}
|
|
|
|
values.put(REACTIONS_UNREAD, 0);
|
|
values.put(REACTIONS_LAST_SEEN, System.currentTimeMillis());
|
|
|
|
db.update(getTableName(), values, whereClause, whereArgs);
|
|
}
|
|
|
|
public void setAllReactionsSeen() {
|
|
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
|
|
ContentValues values = new ContentValues();
|
|
String query = REACTIONS_UNREAD + " != ?";
|
|
String[] args = new String[] { "0" };
|
|
|
|
values.put(REACTIONS_UNREAD, 0);
|
|
values.put(REACTIONS_LAST_SEEN, System.currentTimeMillis());
|
|
|
|
db.update(getTableName(), values, query, args);
|
|
}
|
|
|
|
public void setNotifiedTimestamp(long timestamp, @NonNull List<Long> ids) {
|
|
if (ids.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
|
|
SqlUtil.Query where = SqlUtil.buildSingleCollectionQuery(ID, ids);
|
|
ContentValues values = new ContentValues();
|
|
|
|
values.put(NOTIFIED_TIMESTAMP, timestamp);
|
|
|
|
db.update(getTableName(), values, where.getWhere(), where.getWhereArgs());
|
|
}
|
|
|
|
public void addMismatchedIdentity(long messageId, @NonNull RecipientId recipientId, IdentityKey identityKey) {
|
|
try {
|
|
addToDocument(messageId, MISMATCHED_IDENTITIES,
|
|
new IdentityKeyMismatch(recipientId, identityKey),
|
|
IdentityKeyMismatchSet.class);
|
|
} catch (IOException e) {
|
|
Log.w(TAG, e);
|
|
}
|
|
}
|
|
|
|
public void removeMismatchedIdentity(long messageId, @NonNull RecipientId recipientId, IdentityKey identityKey) {
|
|
try {
|
|
removeFromDocument(messageId, MISMATCHED_IDENTITIES,
|
|
new IdentityKeyMismatch(recipientId, identityKey),
|
|
IdentityKeyMismatchSet.class);
|
|
} catch (IOException e) {
|
|
Log.w(TAG, e);
|
|
}
|
|
}
|
|
|
|
public void setMismatchedIdentities(long messageId, @NonNull Set<IdentityKeyMismatch> mismatches) {
|
|
try {
|
|
setDocument(databaseHelper.getSignalWritableDatabase(), messageId, MISMATCHED_IDENTITIES, new IdentityKeyMismatchSet(mismatches));
|
|
} catch (IOException e) {
|
|
Log.w(TAG, e);
|
|
}
|
|
}
|
|
|
|
public @NonNull List<ReportSpamData> getReportSpamMessageServerGuids(long threadId, long timestamp) {
|
|
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
|
String query = THREAD_ID + " = ? AND " + getDateReceivedColumnName() + " <= ?";
|
|
String[] args = SqlUtil.buildArgs(threadId, timestamp);
|
|
|
|
List<ReportSpamData> data = new ArrayList<>();
|
|
try (Cursor cursor = db.query(getTableName(), new String[] { RECIPIENT_ID, SERVER_GUID, getDateReceivedColumnName() }, query, args, null, null, getDateReceivedColumnName() + " DESC", "3")) {
|
|
while (cursor.moveToNext()) {
|
|
RecipientId id = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID));
|
|
String serverGuid = CursorUtil.requireString(cursor, SERVER_GUID);
|
|
long dateReceived = CursorUtil.requireLong(cursor, getDateReceivedColumnName());
|
|
if (!Util.isEmpty(serverGuid)) {
|
|
data.add(new ReportSpamData(id, serverGuid, dateReceived));
|
|
}
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
|
|
public List<Long> getIncomingPaymentRequestThreads() {
|
|
Cursor cursor = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), "DISTINCT " + THREAD_ID)
|
|
.from(getTableName())
|
|
.where("(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE + " AND (" + getTypeField() + " & ?) != 0", Types.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST)
|
|
.run();
|
|
|
|
return CursorExtensionsKt.readToList(cursor, c -> CursorUtil.requireLong(c, THREAD_ID));
|
|
}
|
|
|
|
@Override
|
|
public void remapRecipient(@NonNull RecipientId fromId, @NonNull RecipientId toId) {
|
|
ContentValues values = new ContentValues();
|
|
values.put(RECIPIENT_ID, toId.serialize());
|
|
getWritableDatabase().update(getTableName(), values, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(toId));
|
|
}
|
|
|
|
@Override
|
|
public void remapThread(long fromId, long toId) {
|
|
ContentValues values = new ContentValues();
|
|
values.put(SmsDatabase.THREAD_ID, toId);
|
|
getWritableDatabase().update(getTableName(), values, THREAD_ID + " = ?", SqlUtil.buildArgs(fromId));
|
|
}
|
|
|
|
void updateReactionsUnread(SQLiteDatabase db, long messageId, boolean hasReactions, boolean isRemoval) {
|
|
try {
|
|
boolean isOutgoing = getMessageRecord(messageId).isOutgoing();
|
|
ContentValues values = new ContentValues();
|
|
|
|
if (!hasReactions) {
|
|
values.put(REACTIONS_UNREAD, 0);
|
|
} else if (!isRemoval) {
|
|
values.put(REACTIONS_UNREAD, 1);
|
|
}
|
|
|
|
if (isOutgoing && hasReactions) {
|
|
values.put(NOTIFIED, 0);
|
|
}
|
|
|
|
if (values.size() > 0) {
|
|
db.update(getTableName(), values, ID_WHERE, SqlUtil.buildArgs(messageId));
|
|
}
|
|
} catch (NoSuchMessageException e) {
|
|
Log.w(TAG, "Failed to find message " + messageId);
|
|
}
|
|
}
|
|
|
|
protected <D extends Document<I>, I> void removeFromDocument(long messageId, String column, I object, Class<D> clazz) throws IOException {
|
|
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
|
|
database.beginTransaction();
|
|
|
|
try {
|
|
D document = getDocument(database, messageId, column, clazz);
|
|
Iterator<I> iterator = document.getItems().iterator();
|
|
|
|
while (iterator.hasNext()) {
|
|
I item = iterator.next();
|
|
|
|
if (item.equals(object)) {
|
|
iterator.remove();
|
|
break;
|
|
}
|
|
}
|
|
|
|
setDocument(database, messageId, column, document);
|
|
database.setTransactionSuccessful();
|
|
} finally {
|
|
database.endTransaction();
|
|
}
|
|
}
|
|
|
|
protected <T extends Document<I>, I> void addToDocument(long messageId, String column, final I object, Class<T> clazz) throws IOException {
|
|
List<I> list = new ArrayList<I>() {{
|
|
add(object);
|
|
}};
|
|
|
|
addToDocument(messageId, column, list, clazz);
|
|
}
|
|
|
|
protected <T extends Document<I>, I> void addToDocument(long messageId, String column, List<I> objects, Class<T> clazz) throws IOException {
|
|
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
|
|
database.beginTransaction();
|
|
|
|
try {
|
|
T document = getDocument(database, messageId, column, clazz);
|
|
document.getItems().addAll(objects);
|
|
setDocument(database, messageId, column, document);
|
|
|
|
database.setTransactionSuccessful();
|
|
} finally {
|
|
database.endTransaction();
|
|
}
|
|
}
|
|
|
|
protected void setDocument(SQLiteDatabase database, long messageId, String column, Document document) throws IOException {
|
|
ContentValues contentValues = new ContentValues();
|
|
|
|
if (document == null || document.size() == 0) {
|
|
contentValues.put(column, (String)null);
|
|
} else {
|
|
contentValues.put(column, JsonUtils.toJson(document));
|
|
}
|
|
|
|
database.update(getTableName(), contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
|
|
}
|
|
|
|
private <D extends Document> D getDocument(SQLiteDatabase database, long messageId,
|
|
String column, Class<D> clazz)
|
|
{
|
|
Cursor cursor = null;
|
|
|
|
try {
|
|
cursor = database.query(getTableName(), new String[] {column},
|
|
ID_WHERE, new String[] {String.valueOf(messageId)},
|
|
null, null, null);
|
|
|
|
if (cursor != null && cursor.moveToNext()) {
|
|
String document = cursor.getString(cursor.getColumnIndexOrThrow(column));
|
|
|
|
try {
|
|
if (!TextUtils.isEmpty(document)) {
|
|
return JsonUtils.fromJson(document, clazz);
|
|
}
|
|
} catch (IOException e) {
|
|
Log.w(TAG, e);
|
|
}
|
|
}
|
|
|
|
try {
|
|
return clazz.newInstance();
|
|
} catch (InstantiationException e) {
|
|
throw new AssertionError(e);
|
|
} catch (IllegalAccessException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
|
|
} finally {
|
|
if (cursor != null)
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
private long getThreadId(@NonNull SQLiteDatabase db, long messageId) {
|
|
String[] projection = new String[]{ THREAD_ID };
|
|
String query = ID + " = ?";
|
|
String[] args = new String[]{ String.valueOf(messageId) };
|
|
|
|
try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null)) {
|
|
if (cursor != null && cursor.moveToFirst()) {
|
|
return cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID));
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
protected enum ReceiptType {
|
|
READ(READ_RECEIPT_COUNT, GroupReceiptDatabase.STATUS_READ),
|
|
DELIVERY(DELIVERY_RECEIPT_COUNT, GroupReceiptDatabase.STATUS_DELIVERED),
|
|
VIEWED(VIEWED_RECEIPT_COUNT, GroupReceiptDatabase.STATUS_VIEWED);
|
|
|
|
private final String columnName;
|
|
private final int groupStatus;
|
|
|
|
ReceiptType(String columnName, int groupStatus) {
|
|
this.columnName = columnName;
|
|
this.groupStatus = groupStatus;
|
|
}
|
|
|
|
public String getColumnName() {
|
|
return columnName;
|
|
}
|
|
|
|
public int getGroupStatus() {
|
|
return groupStatus;
|
|
}
|
|
}
|
|
|
|
public static class SyncMessageId {
|
|
|
|
private final RecipientId recipientId;
|
|
private final long timetamp;
|
|
|
|
public SyncMessageId(@NonNull RecipientId recipientId, long timetamp) {
|
|
this.recipientId = recipientId;
|
|
this.timetamp = timetamp;
|
|
}
|
|
|
|
public RecipientId getRecipientId() {
|
|
return recipientId;
|
|
}
|
|
|
|
public long getTimetamp() {
|
|
return timetamp;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
final SyncMessageId that = (SyncMessageId) o;
|
|
return timetamp == that.timetamp && Objects.equals(recipientId, that.recipientId);
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(recipientId, timetamp);
|
|
}
|
|
}
|
|
|
|
public static class ExpirationInfo {
|
|
|
|
private final long id;
|
|
private final long expiresIn;
|
|
private final long expireStarted;
|
|
private final boolean mms;
|
|
|
|
public ExpirationInfo(long id, long expiresIn, long expireStarted, boolean mms) {
|
|
this.id = id;
|
|
this.expiresIn = expiresIn;
|
|
this.expireStarted = expireStarted;
|
|
this.mms = mms;
|
|
}
|
|
|
|
public long getId() {
|
|
return id;
|
|
}
|
|
|
|
public long getExpiresIn() {
|
|
return expiresIn;
|
|
}
|
|
|
|
public long getExpireStarted() {
|
|
return expireStarted;
|
|
}
|
|
|
|
public boolean isMms() {
|
|
return mms;
|
|
}
|
|
}
|
|
|
|
public static class MarkedMessageInfo {
|
|
|
|
private final long threadId;
|
|
private final SyncMessageId syncMessageId;
|
|
private final MessageId messageId;
|
|
private final ExpirationInfo expirationInfo;
|
|
|
|
public MarkedMessageInfo(long threadId, @NonNull SyncMessageId syncMessageId, @NonNull MessageId messageId, @Nullable ExpirationInfo expirationInfo) {
|
|
this.threadId = threadId;
|
|
this.syncMessageId = syncMessageId;
|
|
this.messageId = messageId;
|
|
this.expirationInfo = expirationInfo;
|
|
}
|
|
|
|
public long getThreadId() {
|
|
return threadId;
|
|
}
|
|
|
|
public @NonNull SyncMessageId getSyncMessageId() {
|
|
return syncMessageId;
|
|
}
|
|
|
|
public @NonNull MessageId getMessageId() {
|
|
return messageId;
|
|
}
|
|
|
|
public @Nullable ExpirationInfo getExpirationInfo() {
|
|
return expirationInfo;
|
|
}
|
|
}
|
|
|
|
public static class InsertResult {
|
|
private final long messageId;
|
|
private final long threadId;
|
|
|
|
public InsertResult(long messageId, long threadId) {
|
|
this.messageId = messageId;
|
|
this.threadId = threadId;
|
|
}
|
|
|
|
public long getMessageId() {
|
|
return messageId;
|
|
}
|
|
|
|
public long getThreadId() {
|
|
return threadId;
|
|
}
|
|
}
|
|
|
|
public static class MmsNotificationInfo {
|
|
private final RecipientId from;
|
|
private final String contentLocation;
|
|
private final String transactionId;
|
|
private final int subscriptionId;
|
|
|
|
MmsNotificationInfo(@NonNull RecipientId from, String contentLocation, String transactionId, int subscriptionId) {
|
|
this.from = from;
|
|
this.contentLocation = contentLocation;
|
|
this.transactionId = transactionId;
|
|
this.subscriptionId = subscriptionId;
|
|
}
|
|
|
|
public String getContentLocation() {
|
|
return contentLocation;
|
|
}
|
|
|
|
public String getTransactionId() {
|
|
return transactionId;
|
|
}
|
|
|
|
public int getSubscriptionId() {
|
|
return subscriptionId;
|
|
}
|
|
|
|
public @NonNull RecipientId getFrom() {
|
|
return from;
|
|
}
|
|
}
|
|
|
|
static class MessageUpdate {
|
|
private final long threadId;
|
|
private final MessageId messageId;
|
|
|
|
MessageUpdate(long threadId, @NonNull MessageId messageId) {
|
|
this.threadId = threadId;
|
|
this.messageId = messageId;
|
|
}
|
|
|
|
public long getThreadId() {
|
|
return threadId;
|
|
}
|
|
|
|
public @NonNull MessageId getMessageId() {
|
|
return messageId;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
final MessageUpdate that = (MessageUpdate) o;
|
|
return threadId == that.threadId && messageId.equals(that.messageId);
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(threadId, messageId);
|
|
}
|
|
}
|
|
|
|
|
|
public interface InsertListener {
|
|
void onComplete();
|
|
}
|
|
|
|
/**
|
|
* Allows the developer to safely iterate over and close a cursor containing
|
|
* data for MessageRecord objects. Supports for-each loops as well as try-with-resources
|
|
* blocks.
|
|
*
|
|
* Readers are considered "one-shot" and it's on the caller to decide what needs
|
|
* to be done with the data. Once read, a reader cannot be read from again. This
|
|
* is by design, since reading data out of a cursor involves object creations and
|
|
* lookups, so it is in the best interest of app performance to only read out the
|
|
* data once. If you need to parse the list multiple times, it is recommended that
|
|
* you copy the iterable out into a normal List, or use extension methods such as
|
|
* partition.
|
|
*
|
|
* This reader does not support removal, since this would be considered a destructive
|
|
* database call.
|
|
*/
|
|
public interface Reader extends Closeable, Iterable<MessageRecord> {
|
|
/**
|
|
* @deprecated Use the Iterable interface instead.
|
|
*/
|
|
@Deprecated
|
|
MessageRecord getNext();
|
|
|
|
/**
|
|
* @deprecated Use the Iterable interface instead.
|
|
*/
|
|
@Deprecated
|
|
MessageRecord getCurrent();
|
|
|
|
/**
|
|
* Pulls the export state out of the query, if it is present.
|
|
*/
|
|
@NonNull MessageExportState getMessageExportStateForCurrentRecord();
|
|
|
|
/**
|
|
* From the {@link Closeable} interface, removing the IOException requirement.
|
|
*/
|
|
void close();
|
|
}
|
|
|
|
public static class ReportSpamData {
|
|
private final RecipientId recipientId;
|
|
private final String serverGuid;
|
|
private final long dateReceived;
|
|
|
|
public ReportSpamData(RecipientId recipientId, String serverGuid, long dateReceived) {
|
|
this.recipientId = recipientId;
|
|
this.serverGuid = serverGuid;
|
|
this.dateReceived = dateReceived;
|
|
}
|
|
|
|
public @NonNull RecipientId getRecipientId() {
|
|
return recipientId;
|
|
}
|
|
|
|
public @NonNull String getServerGuid() {
|
|
return serverGuid;
|
|
}
|
|
|
|
public long getDateReceived() {
|
|
return dateReceived;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Describes which messages to act on. This is used when incrementing receipts.
|
|
* Specifically, this was added to support stories having separate viewed receipt settings.
|
|
*/
|
|
public enum MessageQualifier {
|
|
/**
|
|
* A normal database message (i.e. not a story)
|
|
*/
|
|
NORMAL,
|
|
/**
|
|
* A story message
|
|
*/
|
|
STORY,
|
|
/**
|
|
* Both normal and story message
|
|
*/
|
|
ALL
|
|
}
|
|
}
|