mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-27 22:43:52 +00:00
Prevent group leave event from bumping conversation.
This commit is contained in:
committed by
Alex Hart
parent
b4465953d8
commit
5e968eb831
@@ -236,7 +236,9 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
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_QUIT_BIT + " = 0)";
|
||||
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)) {
|
||||
|
||||
@@ -528,9 +528,9 @@ public class MmsDatabase extends MessageDatabase {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
|
||||
String[] columns = new String[]{ID};
|
||||
String query = ID + " = ? AND " + MESSAGE_BOX + " & ?";
|
||||
long type = Types.getOutgoingEncryptedMessageType() | Types.GROUP_QUIT_BIT;
|
||||
String[] args = new String[]{String.valueOf(messageId), String.valueOf(type)};
|
||||
long type = Types.getOutgoingEncryptedMessageType() | Types.GROUP_LEAVE_BIT;
|
||||
String query = ID + " = ? AND " + MESSAGE_BOX + " & " + type + " = " + type + " AND " + MESSAGE_BOX + " & " + Types.GROUP_V2_BIT + " = 0";
|
||||
String[] args = SqlUtil.buildArgs(messageId);
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, null, null)) {
|
||||
if (cursor.getCount() == 1) {
|
||||
@@ -546,9 +546,9 @@ public class MmsDatabase extends MessageDatabase {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
|
||||
String[] columns = new String[]{DATE_SENT};
|
||||
String query = THREAD_ID + " = ? AND " + MESSAGE_BOX + " & ? AND " + DATE_SENT + " < ?";
|
||||
long type = Types.getOutgoingEncryptedMessageType() | Types.GROUP_QUIT_BIT;
|
||||
String[] args = new String[]{String.valueOf(threadId), String.valueOf(type), String.valueOf(quitTimeBarrier)};
|
||||
long type = Types.getOutgoingEncryptedMessageType() | Types.GROUP_LEAVE_BIT;
|
||||
String query = THREAD_ID + " = ? AND " + MESSAGE_BOX + " & " + type + " = " + type + " AND " + MESSAGE_BOX + " & " + Types.GROUP_V2_BIT + " = 0 AND " + DATE_SENT + " < ?";
|
||||
String[] args = new String[]{String.valueOf(threadId), String.valueOf(quitTimeBarrier)};
|
||||
String orderBy = DATE_SENT + " DESC";
|
||||
String limit = "1";
|
||||
|
||||
@@ -1490,10 +1490,13 @@ public class MmsDatabase extends MessageDatabase {
|
||||
OutgoingGroupUpdateMessage outgoingGroupUpdateMessage = (OutgoingGroupUpdateMessage) message;
|
||||
if (outgoingGroupUpdateMessage.isV2Group()) {
|
||||
type |= Types.GROUP_V2_BIT | Types.GROUP_UPDATE_BIT;
|
||||
if (outgoingGroupUpdateMessage.isJustAGroupLeave()) {
|
||||
type |= Types.GROUP_LEAVE_BIT;
|
||||
}
|
||||
} else {
|
||||
MessageGroupContext.GroupV1Properties properties = outgoingGroupUpdateMessage.requireGroupV1Properties();
|
||||
if (properties.isUpdate()) type |= Types.GROUP_UPDATE_BIT;
|
||||
else if (properties.isQuit()) type |= Types.GROUP_QUIT_BIT;
|
||||
else if (properties.isQuit()) type |= Types.GROUP_LEAVE_BIT;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,9 +118,11 @@ public interface MmsSmsColumns {
|
||||
|
||||
// Group Message Information
|
||||
protected static final long GROUP_UPDATE_BIT = 0x10000;
|
||||
protected static final long GROUP_QUIT_BIT = 0x20000;
|
||||
// Note: Leave bit was previous QUIT bit for GV1, now also general member leave for GV2
|
||||
protected static final long GROUP_LEAVE_BIT = 0x20000;
|
||||
protected static final long EXPIRATION_TIMER_UPDATE_BIT = 0x40000;
|
||||
protected static final long GROUP_V2_BIT = 0x80000;
|
||||
protected static final long GROUP_V2_LEAVE_BITS = GROUP_V2_BIT | GROUP_LEAVE_BIT | GROUP_UPDATE_BIT;
|
||||
|
||||
// Encrypted Storage Information XXX
|
||||
public static final long ENCRYPTION_MASK = 0xFF000000;
|
||||
@@ -303,7 +305,7 @@ public interface MmsSmsColumns {
|
||||
}
|
||||
|
||||
public static boolean isGroupQuit(long type) {
|
||||
return (type & GROUP_QUIT_BIT) != 0;
|
||||
return (type & GROUP_LEAVE_BIT) != 0 && (type & GROUP_V2_BIT) == 0;
|
||||
}
|
||||
|
||||
public static boolean isChatSessionRefresh(long type) {
|
||||
@@ -339,6 +341,10 @@ public interface MmsSmsColumns {
|
||||
return type == CHANGE_NUMBER_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isGroupV2LeaveOnly(long type) {
|
||||
return (type & GROUP_V2_LEAVE_BITS) == GROUP_V2_LEAVE_BITS;
|
||||
}
|
||||
|
||||
public static long translateFromSystemBaseType(long theirType) {
|
||||
// public static final int NONE_TYPE = 0;
|
||||
// public static final int INBOX_TYPE = 1;
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
@@ -47,6 +48,8 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS;
|
||||
|
||||
public class MmsSmsDatabase extends Database {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -107,11 +110,11 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsSmsColumns.VIEWED_RECEIPT_COUNT,
|
||||
MmsSmsColumns.RECEIPT_TIMESTAMP};
|
||||
|
||||
private static final String SNIPPET_QUERY = "SELECT " + MmsSmsColumns.ID + ", 0 AS " + TRANSPORT + ", " + SmsDatabase.TYPE + " AS " + MmsSmsColumns.NORMALIZED_TYPE + ", " + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " FROM " + SmsDatabase.TABLE_NAME + " " +
|
||||
"WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + SmsDatabase.TYPE + " NOT IN (" + SmsDatabase.Types.PROFILE_CHANGE_TYPE + ", " + SmsDatabase.Types.GV1_MIGRATION_TYPE + ", " + SmsDatabase.Types.CHANGE_NUMBER_TYPE + ") " +
|
||||
private static final String SNIPPET_QUERY = "SELECT " + MmsSmsColumns.ID + ", 0 AS " + TRANSPORT + ", " + SmsDatabase.TYPE + " AS " + MmsSmsColumns.NORMALIZED_TYPE + ", " + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " FROM " + SmsDatabase.TABLE_NAME + " " +
|
||||
"WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + SmsDatabase.TYPE + " NOT IN (" + SmsDatabase.Types.PROFILE_CHANGE_TYPE + ", " + SmsDatabase.Types.GV1_MIGRATION_TYPE + ", " + SmsDatabase.Types.CHANGE_NUMBER_TYPE + ") AND " + SmsDatabase.TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " " +
|
||||
"UNION ALL " +
|
||||
"SELECT " + MmsSmsColumns.ID + ", 1 AS " + TRANSPORT + ", " + MmsDatabase.MESSAGE_BOX + " AS " + MmsSmsColumns.NORMALIZED_TYPE + ", " + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " FROM " + MmsDatabase.TABLE_NAME + " " +
|
||||
"WHERE " + MmsSmsColumns.THREAD_ID + " = ? " +
|
||||
"WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + MmsDatabase.MESSAGE_BOX + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " " +
|
||||
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " +
|
||||
"LIMIT 1";
|
||||
|
||||
@@ -220,8 +223,7 @@ public class MmsSmsDatabase extends Database {
|
||||
}
|
||||
|
||||
public @NonNull MessageRecord getConversationSnippet(long threadId) throws NoSuchMessageException {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
try (Cursor cursor = db.rawQuery(SNIPPET_QUERY, SqlUtil.buildArgs(threadId, threadId))) {
|
||||
try (Cursor cursor = getConversationSnippetCursor(threadId)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
boolean isMms = CursorUtil.requireBoolean(cursor, TRANSPORT);
|
||||
long id = CursorUtil.requireLong(cursor, MmsSmsColumns.ID);
|
||||
@@ -237,6 +239,12 @@ public class MmsSmsDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@NonNull Cursor getConversationSnippetCursor(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
return db.rawQuery(SNIPPET_QUERY, SqlUtil.buildArgs(threadId, threadId));
|
||||
}
|
||||
|
||||
public long getConversationSnippetType(long threadId) throws NoSuchMessageException {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
try (Cursor cursor = db.rawQuery(SNIPPET_QUERY, SqlUtil.buildArgs(threadId, threadId))) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.mms.pdu_alt.NotificationInd;
|
||||
@@ -77,6 +78,8 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS;
|
||||
|
||||
/**
|
||||
* Database for storage of SMS messages.
|
||||
*
|
||||
@@ -150,7 +153,8 @@ public class SmsDatabase extends MessageDatabase {
|
||||
REMOTE_DELETED, NOTIFIED_TIMESTAMP, RECEIPT_TIMESTAMP
|
||||
};
|
||||
|
||||
private static final long IGNORABLE_TYPESMASK_WHEN_COUNTING = Types.END_SESSION_BIT | Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT | Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT;
|
||||
@VisibleForTesting
|
||||
static final long IGNORABLE_TYPESMASK_WHEN_COUNTING = Types.END_SESSION_BIT | Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT | Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT;
|
||||
|
||||
private static final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache("SmsDelivery");
|
||||
|
||||
@@ -271,8 +275,8 @@ public class SmsDatabase extends MessageDatabase {
|
||||
}
|
||||
|
||||
private @NonNull SqlUtil.Query buildMeaningfulMessagesQuery(long threadId) {
|
||||
String query = THREAD_ID + " = ? AND (NOT " + TYPE + " & ? AND TYPE != ? AND TYPE != ?)";
|
||||
return SqlUtil.buildQuery(query, threadId, IGNORABLE_TYPESMASK_WHEN_COUNTING, Types.PROFILE_CHANGE_TYPE, Types.CHANGE_NUMBER_TYPE);
|
||||
String query = THREAD_ID + " = ? AND (NOT " + TYPE + " & ? AND " + TYPE + " != ? AND " + TYPE + "!= ? AND NOT " + TYPE + " & ?)";
|
||||
return SqlUtil.buildQuery(query, threadId, IGNORABLE_TYPESMASK_WHEN_COUNTING, Types.PROFILE_CHANGE_TYPE, Types.CHANGE_NUMBER_TYPE, GROUP_V2_LEAVE_BITS);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1088,9 +1092,16 @@ public class SmsDatabase extends MessageDatabase {
|
||||
|
||||
type |= Types.SECURE_MESSAGE_BIT;
|
||||
|
||||
if (incomingGroupUpdateMessage.isGroupV2()) type |= Types.GROUP_V2_BIT | Types.GROUP_UPDATE_BIT;
|
||||
else if (incomingGroupUpdateMessage.isUpdate()) type |= Types.GROUP_UPDATE_BIT;
|
||||
else if (incomingGroupUpdateMessage.isQuit()) type |= Types.GROUP_QUIT_BIT;
|
||||
if (incomingGroupUpdateMessage.isGroupV2()) {
|
||||
type |= Types.GROUP_V2_BIT | Types.GROUP_UPDATE_BIT;
|
||||
if (incomingGroupUpdateMessage.isJustAGroupLeave()) {
|
||||
type |= Types.GROUP_LEAVE_BIT;
|
||||
}
|
||||
} else if (incomingGroupUpdateMessage.isUpdate()) {
|
||||
type |= Types.GROUP_UPDATE_BIT;
|
||||
} else if (incomingGroupUpdateMessage.isQuit()) {
|
||||
type |= Types.GROUP_LEAVE_BIT;
|
||||
}
|
||||
|
||||
} else if (message.isEndSession()) {
|
||||
type |= Types.SECURE_MESSAGE_BIT;
|
||||
|
||||
@@ -1515,7 +1515,8 @@ public class ThreadDatabase extends Database {
|
||||
private boolean isSilentType(long type) {
|
||||
return MmsSmsColumns.Types.isProfileChange(type) ||
|
||||
MmsSmsColumns.Types.isGroupV1MigrationEvent(type) ||
|
||||
MmsSmsColumns.Types.isChangeNumber(type);
|
||||
MmsSmsColumns.Types.isChangeNumber(type) ||
|
||||
MmsSmsColumns.Types.isGroupV2LeaveOnly(type);
|
||||
}
|
||||
|
||||
public Reader readerFor(Cursor cursor) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.GroupV2UpdateMessageUtil;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -70,7 +71,11 @@ public final class OutgoingGroupUpdateMessage extends OutgoingSecureMediaMessage
|
||||
}
|
||||
|
||||
public boolean isV2Group() {
|
||||
return messageGroupContext.isV2Group();
|
||||
return GroupV2UpdateMessageUtil.isGroupV2(messageGroupContext);
|
||||
}
|
||||
|
||||
public boolean isJustAGroupLeave() {
|
||||
return GroupV2UpdateMessageUtil.isJustAGroupLeave(messageGroupContext);
|
||||
}
|
||||
|
||||
public @NonNull MessageGroupContext.GroupV1Properties requireGroupV1Properties() {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms.sms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.thoughtcrime.securesms.mms.MessageGroupContext;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
|
||||
/**
|
||||
* Helper util for inspecting GV2 {@link MessageGroupContext} for various message processing.
|
||||
*/
|
||||
public final class GroupV2UpdateMessageUtil {
|
||||
|
||||
public static boolean isGroupV2(@NonNull MessageGroupContext groupContext) {
|
||||
return groupContext.isV2Group();
|
||||
}
|
||||
|
||||
public static boolean isUpdate(@NonNull MessageGroupContext groupContext) {
|
||||
return groupContext.isV2Group();
|
||||
}
|
||||
|
||||
public static boolean isJustAGroupLeave(@NonNull MessageGroupContext groupContext) {
|
||||
if (isGroupV2(groupContext) && isUpdate(groupContext)) {
|
||||
DecryptedGroupChange decryptedGroupChange = groupContext.requireGroupV2Properties()
|
||||
.getChange();
|
||||
|
||||
return changeEditorOnlyWasRemoved(decryptedGroupChange) &&
|
||||
noChangesOtherThanDeletes(decryptedGroupChange);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean changeEditorOnlyWasRemoved(@NonNull DecryptedGroupChange decryptedGroupChange) {
|
||||
return decryptedGroupChange.getDeleteMembersCount() == 1 &&
|
||||
decryptedGroupChange.getDeleteMembers(0).equals(decryptedGroupChange.getEditor());
|
||||
}
|
||||
|
||||
private static boolean noChangesOtherThanDeletes(@NonNull DecryptedGroupChange decryptedGroupChange) {
|
||||
DecryptedGroupChange withoutDeletedMembers = decryptedGroupChange.toBuilder()
|
||||
.clearDeleteMembers()
|
||||
.build();
|
||||
return DecryptedGroupUtil.changeIsEmpty(withoutDeletedMembers);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
package org.thoughtcrime.securesms.sms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||
import org.thoughtcrime.securesms.mms.MessageGroupContext;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
|
||||
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||
|
||||
@@ -32,39 +28,19 @@ public final class IncomingGroupUpdateMessage extends IncomingTextMessage {
|
||||
}
|
||||
|
||||
public boolean isUpdate() {
|
||||
return groupContext.isV2Group() || groupContext.requireGroupV1Properties().isUpdate();
|
||||
return GroupV2UpdateMessageUtil.isUpdate(groupContext) || groupContext.requireGroupV1Properties().isUpdate();
|
||||
}
|
||||
|
||||
public boolean isGroupV2() {
|
||||
return groupContext.isV2Group();
|
||||
return GroupV2UpdateMessageUtil.isGroupV2(groupContext);
|
||||
}
|
||||
|
||||
public boolean isQuit() {
|
||||
return !groupContext.isV2Group() && groupContext.requireGroupV1Properties().isQuit();
|
||||
return !isGroupV2() && groupContext.requireGroupV1Properties().isQuit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isJustAGroupLeave() {
|
||||
if (isGroupV2() && isUpdate()) {
|
||||
DecryptedGroupChange decryptedGroupChange = groupContext.requireGroupV2Properties()
|
||||
.getChange();
|
||||
|
||||
return changeEditorOnlyWasRemoved(decryptedGroupChange) &&
|
||||
noChangesOtherThanDeletes(decryptedGroupChange);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected boolean changeEditorOnlyWasRemoved(@NonNull DecryptedGroupChange decryptedGroupChange) {
|
||||
return decryptedGroupChange.getDeleteMembersCount() == 1 &&
|
||||
decryptedGroupChange.getDeleteMembers(0).equals(decryptedGroupChange.getEditor());
|
||||
}
|
||||
|
||||
protected boolean noChangesOtherThanDeletes(@NonNull DecryptedGroupChange decryptedGroupChange) {
|
||||
DecryptedGroupChange withoutDeletedMembers = decryptedGroupChange.toBuilder()
|
||||
.clearDeleteMembers()
|
||||
.build();
|
||||
return DecryptedGroupUtil.changeIsEmpty(withoutDeletedMembers);
|
||||
return GroupV2UpdateMessageUtil.isJustAGroupLeave(groupContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.app.Application
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types
|
||||
import org.thoughtcrime.securesms.testing.TestDatabaseUtil
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class MmsDatabaseTest {
|
||||
private lateinit var db: SQLiteDatabase
|
||||
private lateinit var mmsDatabase: MmsDatabase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val sqlCipher = TestDatabaseUtil.inMemoryDatabase {
|
||||
execSQL(MmsDatabase.CREATE_TABLE)
|
||||
}
|
||||
|
||||
db = sqlCipher.writableDatabase
|
||||
mmsDatabase = MmsDatabase(ApplicationProvider.getApplicationContext(), sqlCipher)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isGroupQuitMessage when normal message, return false`() {
|
||||
val id = TestMms.insertMmsMessage(db, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT)
|
||||
assertFalse(mmsDatabase.isGroupQuitMessage(id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isGroupQuitMessage when legacy quit message, return true`() {
|
||||
val id = TestMms.insertMmsMessage(db, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT or Types.GROUP_LEAVE_BIT)
|
||||
assertTrue(mmsDatabase.isGroupQuitMessage(id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isGroupQuitMessage when GV2 leave update, return false`() {
|
||||
val id = TestMms.insertMmsMessage(db, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT or Types.GROUP_LEAVE_BIT or Types.GROUP_V2_BIT or Types.GROUP_UPDATE_BIT)
|
||||
assertFalse(mmsDatabase.isGroupQuitMessage(id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getLatestGroupQuitTimestamp when only normal message, return -1`() {
|
||||
TestMms.insertMmsMessage(db, threadId = 1, sentTimeMillis = 1, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT)
|
||||
assertEquals(-1, mmsDatabase.getLatestGroupQuitTimestamp(1, 4))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getLatestGroupQuitTimestamp when legacy quit, return message timestamp`() {
|
||||
TestMms.insertMmsMessage(db, threadId = 1, sentTimeMillis = 2, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT or Types.GROUP_LEAVE_BIT)
|
||||
assertEquals(2, mmsDatabase.getLatestGroupQuitTimestamp(1, 4))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getLatestGroupQuitTimestamp when GV2 leave update message, return -1`() {
|
||||
TestMms.insertMmsMessage(db, threadId = 1, sentTimeMillis = 3, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT or Types.GROUP_LEAVE_BIT or Types.GROUP_V2_BIT or Types.GROUP_UPDATE_BIT)
|
||||
assertEquals(-1, mmsDatabase.getLatestGroupQuitTimestamp(1, 4))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.app.Application
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.thoughtcrime.securesms.testing.TestDatabaseUtil
|
||||
import org.thoughtcrime.securesms.util.CursorUtil
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class MmsSmsDatabaseTest {
|
||||
|
||||
private lateinit var mmsSmsDatabase: MmsSmsDatabase
|
||||
private lateinit var db: SQLiteDatabase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val sqlCipher = TestDatabaseUtil.inMemoryDatabase {
|
||||
execSQL(MmsDatabase.CREATE_TABLE)
|
||||
execSQL(SmsDatabase.CREATE_TABLE)
|
||||
}
|
||||
|
||||
db = sqlCipher.writableDatabase
|
||||
mmsSmsDatabase = MmsSmsDatabase(ApplicationProvider.getApplicationContext(), sqlCipher)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getConversationSnippet when single normal SMS, return SMS message id and transport as false`() {
|
||||
TestSms.insertSmsMessage(db)
|
||||
mmsSmsDatabase.getConversationSnippetCursor(1).use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
assertEquals(1, CursorUtil.requireLong(cursor, MmsSmsColumns.ID))
|
||||
assertFalse(CursorUtil.requireBoolean(cursor, MmsSmsDatabase.TRANSPORT))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getConversationSnippet when single normal MMS, return MMS message id and transport as true`() {
|
||||
TestMms.insertMmsMessage(db)
|
||||
mmsSmsDatabase.getConversationSnippetCursor(1).use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
assertEquals(1, CursorUtil.requireLong(cursor, MmsSmsColumns.ID))
|
||||
assertTrue(CursorUtil.requireBoolean(cursor, MmsSmsDatabase.TRANSPORT))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getConversationSnippet when single normal MMS then GV2 leave update message, return MMS message id and transport as true both times`() {
|
||||
val timestamp = System.currentTimeMillis()
|
||||
|
||||
TestMms.insertMmsMessage(db, receivedTimestampMillis = timestamp + 2)
|
||||
mmsSmsDatabase.getConversationSnippetCursor(1).use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
assertEquals(1, CursorUtil.requireLong(cursor, MmsSmsColumns.ID))
|
||||
assertTrue(CursorUtil.requireBoolean(cursor, MmsSmsDatabase.TRANSPORT))
|
||||
}
|
||||
|
||||
TestSms.insertSmsMessage(db, receivedTimestampMillis = timestamp + 3, type = MmsSmsColumns.Types.BASE_SENDING_TYPE or MmsSmsColumns.Types.SECURE_MESSAGE_BIT or MmsSmsColumns.Types.PUSH_MESSAGE_BIT or MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS)
|
||||
mmsSmsDatabase.getConversationSnippetCursor(1).use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
assertEquals(1, CursorUtil.requireLong(cursor, MmsSmsColumns.ID))
|
||||
assertTrue(CursorUtil.requireBoolean(cursor, MmsSmsDatabase.TRANSPORT))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.app.Application
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.thoughtcrime.securesms.testing.TestDatabaseUtil
|
||||
import android.database.sqlite.SQLiteDatabase as AndroidSQLiteDatabase
|
||||
import org.hamcrest.CoreMatchers.`is` as isEqual
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class SmsDatabaseTest {
|
||||
|
||||
private lateinit var writeableDatabase: AndroidSQLiteDatabase
|
||||
private lateinit var smsDatabase: SmsDatabase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val sqlCipher = TestDatabaseUtil.inMemoryDatabase {
|
||||
execSQL(SmsDatabase.CREATE_TABLE)
|
||||
}
|
||||
|
||||
writeableDatabase = sqlCipher.writableDatabase
|
||||
smsDatabase = SmsDatabase(ApplicationProvider.getApplicationContext(), sqlCipher)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
writeableDatabase.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getThreadIdForMessage when no message absent for id, return -1`() {
|
||||
assertThat(smsDatabase.getThreadIdForMessage(1), isEqual(-1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getThreadIdForMessage when message present for id, return thread id`() {
|
||||
TestSms.insertSmsMessage(db = writeableDatabase, threadId = 1)
|
||||
assertThat(smsDatabase.getThreadIdForMessage(1), isEqual(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasMeaningfulMessage when no messages, return false`() {
|
||||
assertFalse(smsDatabase.hasMeaningfulMessage(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasMeaningfulMessage when normal message, return true`() {
|
||||
TestSms.insertSmsMessage(db = writeableDatabase, threadId = 1)
|
||||
assertTrue(smsDatabase.hasMeaningfulMessage(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasMeaningfulMessage when empty and then with ignored types, always return false`() {
|
||||
assertFalse(smsDatabase.hasMeaningfulMessage(1))
|
||||
|
||||
TestSms.insertSmsMessage(db = writeableDatabase, threadId = 1, type = SmsDatabase.IGNORABLE_TYPESMASK_WHEN_COUNTING)
|
||||
assertFalse(smsDatabase.hasMeaningfulMessage(1))
|
||||
|
||||
TestSms.insertSmsMessage(db = writeableDatabase, threadId = 1, type = MmsSmsColumns.Types.PROFILE_CHANGE_TYPE)
|
||||
assertFalse(smsDatabase.hasMeaningfulMessage(1))
|
||||
|
||||
TestSms.insertSmsMessage(db = writeableDatabase, threadId = 1, type = MmsSmsColumns.Types.CHANGE_NUMBER_TYPE)
|
||||
assertFalse(smsDatabase.hasMeaningfulMessage(1))
|
||||
|
||||
TestSms.insertSmsMessage(db = writeableDatabase, threadId = 1, type = MmsSmsColumns.Types.BASE_INBOX_TYPE or MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS)
|
||||
assertFalse(smsDatabase.hasMeaningfulMessage(1))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import com.google.android.mms.pdu_alt.PduHeaders
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Helper methods for inserting an MMS message into the MMS table.
|
||||
*/
|
||||
object TestMms {
|
||||
|
||||
fun insertMmsMessage(
|
||||
db: SQLiteDatabase,
|
||||
recipient: Recipient = Recipient.UNKNOWN,
|
||||
body: String = "body",
|
||||
sentTimeMillis: Long = System.currentTimeMillis(),
|
||||
receivedTimestampMillis: Long = System.currentTimeMillis(),
|
||||
subscriptionId: Int = -1,
|
||||
expiresIn: Long = 0,
|
||||
viewOnce: Boolean = false,
|
||||
distributionType: Int = ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE,
|
||||
unread: Boolean = false,
|
||||
threadId: Long = 1
|
||||
): Long {
|
||||
val message = OutgoingMediaMessage(
|
||||
recipient,
|
||||
body,
|
||||
emptyList(),
|
||||
sentTimeMillis,
|
||||
subscriptionId,
|
||||
expiresIn,
|
||||
viewOnce,
|
||||
distributionType,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList()
|
||||
)
|
||||
|
||||
return insertMmsMessage(
|
||||
db = db,
|
||||
message = message,
|
||||
body = body,
|
||||
type = type,
|
||||
unread = unread,
|
||||
threadId = threadId,
|
||||
receivedTimestampMillis = receivedTimestampMillis
|
||||
)
|
||||
}
|
||||
|
||||
fun insertMmsMessage(
|
||||
db: SQLiteDatabase,
|
||||
message: OutgoingMediaMessage,
|
||||
body: String = message.body,
|
||||
type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE,
|
||||
unread: Boolean = false,
|
||||
threadId: Long = 1,
|
||||
receivedTimestampMillis: Long = System.currentTimeMillis(),
|
||||
): Long {
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(MmsDatabase.DATE_SENT, message.sentTimeMillis)
|
||||
contentValues.put(MmsDatabase.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ)
|
||||
|
||||
contentValues.put(MmsDatabase.MESSAGE_BOX, type)
|
||||
contentValues.put(MmsSmsColumns.THREAD_ID, threadId)
|
||||
contentValues.put(MmsSmsColumns.READ, if (unread) 0 else 1)
|
||||
contentValues.put(MmsDatabase.DATE_RECEIVED, receivedTimestampMillis)
|
||||
contentValues.put(MmsSmsColumns.SUBSCRIPTION_ID, message.subscriptionId)
|
||||
contentValues.put(MmsSmsColumns.EXPIRES_IN, message.expiresIn)
|
||||
contentValues.put(MmsDatabase.VIEW_ONCE, message.isViewOnce)
|
||||
contentValues.put(MmsSmsColumns.RECIPIENT_ID, message.recipient.id.serialize())
|
||||
contentValues.put(MmsSmsColumns.DELIVERY_RECEIPT_COUNT, 0)
|
||||
contentValues.put(MmsSmsColumns.RECEIPT_TIMESTAMP, 0)
|
||||
|
||||
contentValues.put(MmsSmsColumns.BODY, body)
|
||||
contentValues.put(MmsDatabase.PART_COUNT, 0)
|
||||
contentValues.put(MmsDatabase.MENTIONS_SELF, 0)
|
||||
|
||||
return db.insert(MmsDatabase.TABLE_NAME, null, contentValues)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.text.TextUtils
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import java.util.UUID
|
||||
import android.database.sqlite.SQLiteDatabase as AndroidSQLiteDatabase
|
||||
|
||||
/**
|
||||
* Helper methods for inserting SMS messages into the SMS table.
|
||||
*/
|
||||
object TestSms {
|
||||
|
||||
fun insertSmsMessage(
|
||||
db: AndroidSQLiteDatabase,
|
||||
sender: RecipientId = RecipientId.from(1),
|
||||
senderDeviceId: Int = 1,
|
||||
sentTimestampMillis: Long = System.currentTimeMillis(),
|
||||
serverTimestampMillis: Long = System.currentTimeMillis(),
|
||||
receivedTimestampMillis: Long = System.currentTimeMillis(),
|
||||
encodedBody: String = "encodedBody",
|
||||
groupId: Optional<GroupId> = Optional.absent(),
|
||||
expiresInMillis: Long = 0,
|
||||
unidentified: Boolean = false,
|
||||
serverGuid: String = UUID.randomUUID().toString(),
|
||||
type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE,
|
||||
unread: Boolean = false,
|
||||
threadId: Long = 1
|
||||
): Long {
|
||||
val message = IncomingTextMessage(
|
||||
sender,
|
||||
senderDeviceId,
|
||||
sentTimestampMillis,
|
||||
serverTimestampMillis,
|
||||
receivedTimestampMillis,
|
||||
encodedBody,
|
||||
groupId,
|
||||
expiresInMillis,
|
||||
unidentified,
|
||||
serverGuid
|
||||
)
|
||||
|
||||
return insertSmsMessage(
|
||||
db = db,
|
||||
message = message,
|
||||
type = type,
|
||||
unread = unread,
|
||||
threadId = threadId
|
||||
)
|
||||
}
|
||||
|
||||
fun insertSmsMessage(
|
||||
db: AndroidSQLiteDatabase,
|
||||
message: IncomingTextMessage,
|
||||
type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE,
|
||||
unread: Boolean = false,
|
||||
threadId: Long = 1
|
||||
): Long {
|
||||
val values = ContentValues()
|
||||
values.put(MmsSmsColumns.RECIPIENT_ID, message.sender.serialize())
|
||||
values.put(MmsSmsColumns.ADDRESS_DEVICE_ID, message.senderDeviceId)
|
||||
values.put(SmsDatabase.DATE_RECEIVED, message.receivedTimestampMillis)
|
||||
values.put(SmsDatabase.DATE_SENT, message.sentTimestampMillis)
|
||||
values.put(MmsSmsColumns.DATE_SERVER, message.serverTimestampMillis)
|
||||
values.put(SmsDatabase.PROTOCOL, message.protocol)
|
||||
values.put(MmsSmsColumns.READ, if (unread) 0 else 1)
|
||||
values.put(MmsSmsColumns.SUBSCRIPTION_ID, message.subscriptionId)
|
||||
values.put(MmsSmsColumns.EXPIRES_IN, message.expiresIn)
|
||||
values.put(MmsSmsColumns.UNIDENTIFIED, message.isUnidentified)
|
||||
|
||||
if (!TextUtils.isEmpty(message.pseudoSubject)) values.put(SmsDatabase.SUBJECT, message.pseudoSubject)
|
||||
|
||||
values.put(SmsDatabase.REPLY_PATH_PRESENT, message.isReplyPathPresent)
|
||||
values.put(SmsDatabase.SERVICE_CENTER, message.serviceCenterAddress)
|
||||
values.put(MmsSmsColumns.BODY, message.messageBody)
|
||||
values.put(SmsDatabase.TYPE, type)
|
||||
values.put(MmsSmsColumns.THREAD_ID, threadId)
|
||||
values.put(MmsSmsColumns.SERVER_GUID, message.serverGuid)
|
||||
|
||||
return db.insert(SmsDatabase.TABLE_NAME, null, values)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.thoughtcrime.securesms.sms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||
import org.thoughtcrime.securesms.groups.v2.ChangeBuilder;
|
||||
import org.thoughtcrime.securesms.mms.MessageGroupContext;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class GroupV2UpdateMessageUtilTest {
|
||||
|
||||
@Test
|
||||
public void isJustAGroupLeave_whenEditorIsRemoved_shouldReturnTrue() {
|
||||
// GIVEN
|
||||
UUID alice = UUID.randomUUID();
|
||||
DecryptedGroupChange change = ChangeBuilder.changeBy(alice)
|
||||
.deleteMember(alice)
|
||||
.build();
|
||||
|
||||
DecryptedGroupV2Context context = DecryptedGroupV2Context.newBuilder()
|
||||
.setContext(SignalServiceProtos.GroupContextV2.newBuilder()
|
||||
.setMasterKey(ByteString.copyFrom(randomBytes())))
|
||||
.setChange(change)
|
||||
.build();
|
||||
|
||||
MessageGroupContext messageGroupContext = new MessageGroupContext(context);
|
||||
|
||||
// WHEN
|
||||
boolean isJustAGroupLeave = GroupV2UpdateMessageUtil.isJustAGroupLeave(messageGroupContext);
|
||||
|
||||
// THEN
|
||||
assertTrue(isJustAGroupLeave);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isJustAGroupLeave_whenOtherIsRemoved_shouldReturnFalse() {
|
||||
// GIVEN
|
||||
UUID alice = UUID.randomUUID();
|
||||
UUID bob = UUID.randomUUID();
|
||||
DecryptedGroupChange change = ChangeBuilder.changeBy(alice)
|
||||
.deleteMember(bob)
|
||||
.build();
|
||||
|
||||
DecryptedGroupV2Context context = DecryptedGroupV2Context.newBuilder()
|
||||
.setContext(SignalServiceProtos.GroupContextV2.newBuilder()
|
||||
.setMasterKey(ByteString.copyFrom(randomBytes())))
|
||||
.setChange(change)
|
||||
.build();
|
||||
|
||||
MessageGroupContext messageGroupContext = new MessageGroupContext(context);
|
||||
|
||||
// WHEN
|
||||
boolean isJustAGroupLeave = GroupV2UpdateMessageUtil.isJustAGroupLeave(messageGroupContext);
|
||||
|
||||
// THEN
|
||||
assertFalse(isJustAGroupLeave);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isJustAGroupLeave_whenEditorIsRemovedAndOtherChanges_shouldReturnFalse() {
|
||||
// GIVEN
|
||||
UUID alice = UUID.randomUUID();
|
||||
UUID bob = UUID.randomUUID();
|
||||
DecryptedGroupChange change = ChangeBuilder.changeBy(alice)
|
||||
.deleteMember(alice)
|
||||
.addMember(bob)
|
||||
.build();
|
||||
|
||||
DecryptedGroupV2Context context = DecryptedGroupV2Context.newBuilder()
|
||||
.setContext(SignalServiceProtos.GroupContextV2.newBuilder()
|
||||
.setMasterKey(ByteString.copyFrom(randomBytes())))
|
||||
.setChange(change)
|
||||
.build();
|
||||
|
||||
MessageGroupContext messageGroupContext = new MessageGroupContext(context);
|
||||
|
||||
// WHEN
|
||||
boolean isJustAGroupLeave = GroupV2UpdateMessageUtil.isJustAGroupLeave(messageGroupContext);
|
||||
|
||||
// THEN
|
||||
assertFalse(isJustAGroupLeave);
|
||||
}
|
||||
|
||||
private @NonNull byte[] randomBytes() {
|
||||
byte[] bytes = new byte[32];
|
||||
new Random().nextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import java.security.SecureRandom
|
||||
import android.database.sqlite.SQLiteDatabase as AndroidSQLiteDatabase
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase as SQLCipherSQLiteDatabase
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSQLiteDatabase
|
||||
|
||||
/**
|
||||
* Proxy [SQLCipherOpenHelper] to the [TestSQLiteOpenHelper] interface.
|
||||
*/
|
||||
class ProxySQLCipherOpenHelper(
|
||||
context: Context,
|
||||
val readableDatabase: AndroidSQLiteDatabase,
|
||||
val writableDatabase: AndroidSQLiteDatabase,
|
||||
) : SQLCipherOpenHelper(context, DatabaseSecret(ByteArray(32).apply { SecureRandom().nextBytes(this) })) {
|
||||
|
||||
constructor(context: Context, testOpenHelper: TestSQLiteOpenHelper) : this(context, testOpenHelper.readableDatabase, testOpenHelper.writableDatabase)
|
||||
|
||||
override fun close() {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getDatabaseName(): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun setWriteAheadLoggingEnabled(enabled: Boolean) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun onConfigure(db: SQLCipherSQLiteDatabase) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun onBeforeDelete(db: SQLCipherSQLiteDatabase?) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun onDowngrade(db: SQLCipherSQLiteDatabase?, oldVersion: Int, newVersion: Int) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun onOpen(db: SQLCipherSQLiteDatabase?) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun onCreate(db: SQLCipherSQLiteDatabase?) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLCipherSQLiteDatabase?, oldVersion: Int, newVersion: Int) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getReadableDatabase(): SQLCipherSQLiteDatabase {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getWritableDatabase(): SQLCipherSQLiteDatabase {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getRawReadableDatabase(): SQLCipherSQLiteDatabase {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getRawWritableDatabase(): SQLCipherSQLiteDatabase {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getSignalReadableDatabase(): SignalSQLiteDatabase {
|
||||
return ProxySignalSQLiteDatabase(readableDatabase)
|
||||
}
|
||||
|
||||
override fun getSignalWritableDatabase(): SignalSQLiteDatabase {
|
||||
return ProxySignalSQLiteDatabase(writableDatabase)
|
||||
}
|
||||
|
||||
override fun getSqlCipherDatabase(): SQLCipherSQLiteDatabase {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun markCurrent(db: SQLCipherSQLiteDatabase?) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import java.util.Locale
|
||||
import android.database.sqlite.SQLiteDatabase as AndroidSQLiteDatabase
|
||||
import android.database.sqlite.SQLiteTransactionListener as AndroidSQLiteTransactionListener
|
||||
import net.zetetic.database.sqlcipher.SQLiteStatement as SQLCipherSQLiteStatement
|
||||
import net.zetetic.database.sqlcipher.SQLiteTransactionListener as SQLCipherSQLiteTransactionListener
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSQLiteDatabase
|
||||
|
||||
/**
|
||||
* Partial implementation of [SignalSQLiteDatabase] using an instance of [AndroidSQLiteDatabase] instead
|
||||
* of SQLCipher.
|
||||
*/
|
||||
class ProxySignalSQLiteDatabase(private val database: AndroidSQLiteDatabase) : SignalSQLiteDatabase(null) {
|
||||
override fun getSqlCipherDatabase(): net.zetetic.database.sqlcipher.SQLiteDatabase {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun beginTransaction() {
|
||||
database.beginTransaction()
|
||||
}
|
||||
|
||||
override fun endTransaction() {
|
||||
database.endTransaction()
|
||||
}
|
||||
|
||||
override fun setTransactionSuccessful() {
|
||||
database.setTransactionSuccessful()
|
||||
}
|
||||
|
||||
override fun query(distinct: Boolean, table: String?, columns: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, groupBy: String?, having: String?, orderBy: String?, limit: String?): Cursor {
|
||||
return database.query(distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit)
|
||||
}
|
||||
|
||||
override fun queryWithFactory(
|
||||
cursorFactory: net.zetetic.database.sqlcipher.SQLiteDatabase.CursorFactory?,
|
||||
distinct: Boolean,
|
||||
table: String?,
|
||||
columns: Array<out String>?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<out String>?,
|
||||
groupBy: String?,
|
||||
having: String?,
|
||||
orderBy: String?,
|
||||
limit: String?
|
||||
): Cursor {
|
||||
return database.queryWithFactory(null, distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit)
|
||||
}
|
||||
|
||||
override fun query(table: String?, columns: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, groupBy: String?, having: String?, orderBy: String?): Cursor {
|
||||
return database.query(table, columns, selection, selectionArgs, groupBy, having, orderBy)
|
||||
}
|
||||
|
||||
override fun query(table: String?, columns: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, groupBy: String?, having: String?, orderBy: String?, limit: String?): Cursor {
|
||||
return database.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit)
|
||||
}
|
||||
|
||||
override fun rawQuery(sql: String?, selectionArgs: Array<out String>?): Cursor {
|
||||
return database.rawQuery(sql, selectionArgs)
|
||||
}
|
||||
|
||||
override fun rawQuery(sql: String?, args: Array<out Any>?): Cursor {
|
||||
return database.rawQuery(sql, args?.map(Any::toString)?.toTypedArray())
|
||||
}
|
||||
|
||||
override fun rawQueryWithFactory(cursorFactory: net.zetetic.database.sqlcipher.SQLiteDatabase.CursorFactory?, sql: String?, selectionArgs: Array<out String>?, editTable: String?): Cursor {
|
||||
return database.rawQueryWithFactory(null, sql, selectionArgs, editTable)
|
||||
}
|
||||
|
||||
override fun rawQuery(sql: String?, selectionArgs: Array<out String>?, initialRead: Int, maxRead: Int): Cursor {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun insert(table: String?, nullColumnHack: String?, values: ContentValues?): Long {
|
||||
return database.insert(table, nullColumnHack, values)
|
||||
}
|
||||
|
||||
override fun insertOrThrow(table: String?, nullColumnHack: String?, values: ContentValues?): Long {
|
||||
return database.insertOrThrow(table, nullColumnHack, values)
|
||||
}
|
||||
|
||||
override fun replace(table: String?, nullColumnHack: String?, initialValues: ContentValues?): Long {
|
||||
return database.replace(table, nullColumnHack, initialValues)
|
||||
}
|
||||
|
||||
override fun replaceOrThrow(table: String?, nullColumnHack: String?, initialValues: ContentValues?): Long {
|
||||
return database.replaceOrThrow(table, nullColumnHack, initialValues)
|
||||
}
|
||||
|
||||
override fun insertWithOnConflict(table: String?, nullColumnHack: String?, initialValues: ContentValues?, conflictAlgorithm: Int): Long {
|
||||
return database.insertWithOnConflict(table, nullColumnHack, initialValues, conflictAlgorithm)
|
||||
}
|
||||
|
||||
override fun delete(table: String?, whereClause: String?, whereArgs: Array<out String>?): Int {
|
||||
return database.delete(table, whereClause, whereArgs)
|
||||
}
|
||||
|
||||
override fun update(table: String?, values: ContentValues?, whereClause: String?, whereArgs: Array<out String>?): Int {
|
||||
return database.update(table, values, whereClause, whereArgs)
|
||||
}
|
||||
|
||||
override fun updateWithOnConflict(table: String?, values: ContentValues?, whereClause: String?, whereArgs: Array<out String>?, conflictAlgorithm: Int): Int {
|
||||
return database.updateWithOnConflict(table, values, whereClause, whereArgs, conflictAlgorithm)
|
||||
}
|
||||
|
||||
override fun execSQL(sql: String?) {
|
||||
database.execSQL(sql)
|
||||
}
|
||||
|
||||
override fun rawExecSQL(sql: String?) {
|
||||
database.execSQL(sql)
|
||||
}
|
||||
|
||||
override fun execSQL(sql: String?, bindArgs: Array<out Any>?) {
|
||||
database.execSQL(sql, bindArgs)
|
||||
}
|
||||
|
||||
override fun enableWriteAheadLogging(): Boolean {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun disableWriteAheadLogging() {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun isWriteAheadLoggingEnabled(): Boolean {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun setForeignKeyConstraintsEnabled(enable: Boolean) {
|
||||
database.setForeignKeyConstraintsEnabled(enable)
|
||||
}
|
||||
|
||||
override fun beginTransactionWithListener(transactionListener: SQLCipherSQLiteTransactionListener?) {
|
||||
database.beginTransactionWithListener(object : AndroidSQLiteTransactionListener {
|
||||
override fun onBegin() {
|
||||
transactionListener?.onBegin()
|
||||
}
|
||||
|
||||
override fun onCommit() {
|
||||
transactionListener?.onCommit()
|
||||
}
|
||||
|
||||
override fun onRollback() {
|
||||
transactionListener?.onRollback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun beginTransactionNonExclusive() {
|
||||
database.beginTransactionNonExclusive()
|
||||
}
|
||||
|
||||
override fun beginTransactionWithListenerNonExclusive(transactionListener: SQLCipherSQLiteTransactionListener?) {
|
||||
database.beginTransactionWithListenerNonExclusive(object : AndroidSQLiteTransactionListener {
|
||||
override fun onBegin() {
|
||||
transactionListener?.onBegin()
|
||||
}
|
||||
|
||||
override fun onCommit() {
|
||||
transactionListener?.onCommit()
|
||||
}
|
||||
|
||||
override fun onRollback() {
|
||||
transactionListener?.onRollback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun inTransaction(): Boolean {
|
||||
return database.inTransaction()
|
||||
}
|
||||
|
||||
override fun isDbLockedByCurrentThread(): Boolean {
|
||||
return database.isDbLockedByCurrentThread
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun isDbLockedByOtherThreads(): Boolean {
|
||||
return database.isDbLockedByOtherThreads
|
||||
}
|
||||
|
||||
override fun yieldIfContendedSafely(): Boolean {
|
||||
return database.yieldIfContendedSafely()
|
||||
}
|
||||
|
||||
override fun yieldIfContendedSafely(sleepAfterYieldDelay: Long): Boolean {
|
||||
return database.yieldIfContendedSafely(sleepAfterYieldDelay)
|
||||
}
|
||||
|
||||
override fun getVersion(): Int {
|
||||
return database.version
|
||||
}
|
||||
|
||||
override fun setVersion(version: Int) {
|
||||
database.version = version
|
||||
}
|
||||
|
||||
override fun getMaximumSize(): Long {
|
||||
return database.maximumSize
|
||||
}
|
||||
|
||||
override fun setMaximumSize(numBytes: Long): Long {
|
||||
return database.setMaximumSize(numBytes)
|
||||
}
|
||||
|
||||
override fun getPageSize(): Long {
|
||||
return database.pageSize
|
||||
}
|
||||
|
||||
override fun setPageSize(numBytes: Long) {
|
||||
database.pageSize = numBytes
|
||||
}
|
||||
|
||||
override fun compileStatement(sql: String?): SQLCipherSQLiteStatement {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun isReadOnly(): Boolean {
|
||||
return database.isReadOnly
|
||||
}
|
||||
|
||||
override fun isOpen(): Boolean {
|
||||
return database.isOpen
|
||||
}
|
||||
|
||||
override fun needUpgrade(newVersion: Int): Boolean {
|
||||
return database.needUpgrade(newVersion)
|
||||
}
|
||||
|
||||
override fun setLocale(locale: Locale?) {
|
||||
database.setLocale(locale)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Helper for creating/reading a database for unit tests.
|
||||
*/
|
||||
object TestDatabaseUtil {
|
||||
|
||||
/**
|
||||
* Create an in-memory only database that is empty. Can pass [onCreate] to do similar operations
|
||||
* one would do in a open helper's onCreate.
|
||||
*/
|
||||
fun inMemoryDatabase(onCreate: OnCreate): ProxySQLCipherOpenHelper {
|
||||
val testSQLiteOpenHelper = TestSQLiteOpenHelper(ApplicationProvider.getApplicationContext(), onCreate)
|
||||
return ProxySQLCipherOpenHelper(ApplicationProvider.getApplicationContext(), testSQLiteOpenHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a database file located in app/src/test/resources/db. Currently only reads
|
||||
* are allowed due to weird caching of the file resulting in non-deterministic tests.
|
||||
*/
|
||||
fun fromFileDatabase(name: String): ProxySQLCipherOpenHelper {
|
||||
val databaseFile = File(javaClass.getResource("/db/$name")!!.file)
|
||||
val sqliteDatabase = SQLiteDatabase.openDatabase(databaseFile.absolutePath, null, SQLiteDatabase.OPEN_READONLY)
|
||||
return ProxySQLCipherOpenHelper(ApplicationProvider.getApplicationContext(), sqliteDatabase, sqliteDatabase)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase as AndroidSQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper as AndroidSQLiteOpenHelper
|
||||
|
||||
typealias OnCreate = AndroidSQLiteDatabase.() -> Unit
|
||||
|
||||
/**
|
||||
* [AndroidSQLiteOpenHelper] for use in unit tests.
|
||||
*/
|
||||
class TestSQLiteOpenHelper(context: Context, private val onCreate: OnCreate) : AndroidSQLiteOpenHelper(context, "test", null, 1) {
|
||||
|
||||
fun setup() {
|
||||
onCreate(writableDatabase)
|
||||
}
|
||||
|
||||
override fun onCreate(db: AndroidSQLiteDatabase) {
|
||||
onCreate.invoke(db)
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: AndroidSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
// no upgrade
|
||||
}
|
||||
}
|
||||
BIN
app/src/test/resources/db/test.sqlite
Normal file
BIN
app/src/test/resources/db/test.sqlite
Normal file
Binary file not shown.
Reference in New Issue
Block a user