package org.thoughtcrime.securesms.database; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.text.TextUtils; import net.zetetic.database.sqlcipher.SQLiteDatabase; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Document; import org.session.libsession.utilities.IdentityKeyMismatch; import org.session.libsession.utilities.IdentityKeyMismatchList; import org.session.libsignal.crypto.IdentityKey; import org.session.libsignal.utilities.JsonUtil; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.util.SqlUtil; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public abstract class MessagingDatabase extends Database implements MmsSmsColumns { private static final String TAG = MessagingDatabase.class.getSimpleName(); public MessagingDatabase(Context context, SQLCipherOpenHelper databaseHelper) { super(context, databaseHelper); } protected abstract String getTableName(); public abstract void markExpireStarted(long messageId); public abstract void markExpireStarted(long messageId, long startTime); public abstract void markAsSent(long messageId, boolean secure); public abstract void markAsSyncing(long id); public abstract void markAsResyncing(long id); public abstract void markAsSyncFailed(long id); public abstract void markUnidentified(long messageId, boolean unidentified); public abstract void markAsDeleted(long messageId, boolean read, boolean hasMention); public abstract boolean deleteMessage(long messageId); public abstract boolean deleteMessages(long[] messageId, long threadId); public abstract void updateThreadId(long fromId, long toId); public abstract MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException; public void addMismatchedIdentity(long messageId, Address address, IdentityKey identityKey) { try { addToDocument(messageId, MISMATCHED_IDENTITIES, new IdentityKeyMismatch(address, identityKey), IdentityKeyMismatchList.class); } catch (IOException e) { Log.w(TAG, e); } } public void removeMismatchedIdentity(long messageId, Address address, IdentityKey identityKey) { try { removeFromDocument(messageId, MISMATCHED_IDENTITIES, new IdentityKeyMismatch(address, identityKey), IdentityKeyMismatchList.class); } catch (IOException e) { Log.w(TAG, e); } } void updateReactionsUnread(SQLiteDatabase db, long messageId, boolean hasReactions, boolean isRemoval, boolean notifyUnread) { try { MessageRecord message = getMessageRecord(messageId); ContentValues values = new ContentValues(); if (notifyUnread) { if (!hasReactions) { values.put(REACTIONS_UNREAD, 0); } else if (!isRemoval) { values.put(REACTIONS_UNREAD, 1); } } else { values.put(REACTIONS_UNREAD, 0); } if (message.isOutgoing() && hasReactions) { values.put(NOTIFIED, 0); } if (values.size() > 0) { db.update(getTableName(), values, ID_WHERE, SqlUtil.buildArgs(messageId)); } notifyConversationListeners(message.getThreadId()); } catch (NoSuchMessageException e) { Log.w(TAG, "Failed to find message " + messageId); } } protected , I> void removeFromDocument(long messageId, String column, I object, Class clazz) throws IOException { SQLiteDatabase database = databaseHelper.getWritableDatabase(); database.beginTransaction(); try { D document = getDocument(database, messageId, column, clazz); Iterator iterator = document.getList().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 , I> void addToDocument(long messageId, String column, final I object, Class clazz) throws IOException { List list = new ArrayList() {{ add(object); }}; addToDocument(messageId, column, list, clazz); } protected , I> void addToDocument(long messageId, String column, List objects, Class clazz) throws IOException { SQLiteDatabase database = databaseHelper.getWritableDatabase(); database.beginTransaction(); try { T document = getDocument(database, messageId, column, clazz); document.getList().addAll(objects); setDocument(database, messageId, column, document); database.setTransactionSuccessful(); } finally { database.endTransaction(); } } private 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, JsonUtil.toJsonThrows(document)); } database.update(getTableName(), contentValues, ID_WHERE, new String[] {String.valueOf(messageId)}); } private D getDocument(SQLiteDatabase database, long messageId, String column, Class 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 JsonUtil.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(); } } public void migrateThreadId(long oldThreadId, long newThreadId) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); String where = THREAD_ID+" = ?"; String[] args = new String[]{oldThreadId+""}; ContentValues contentValues = new ContentValues(); contentValues.put(THREAD_ID, newThreadId); db.update(getTableName(), contentValues, where, args); } public static class SyncMessageId { private final Address address; private final long timetamp; public SyncMessageId(Address address, long timetamp) { this.address = address; this.timetamp = timetamp; } public Address getAddress() { return address; } public long getTimetamp() { return timetamp; } } 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; } } }