kopia lustrzana https://github.com/ryukoposting/Signal-Android
Fix out-of-sync local state after rejoining a group via invite link.
rodzic
3895578d51
commit
26709177d2
|
@ -199,6 +199,7 @@ import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestio
|
||||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||||
import org.thoughtcrime.securesms.invites.InviteReminderModel;
|
import org.thoughtcrime.securesms.invites.InviteReminderModel;
|
||||||
import org.thoughtcrime.securesms.invites.InviteReminderRepository;
|
import org.thoughtcrime.securesms.invites.InviteReminderRepository;
|
||||||
|
import org.thoughtcrime.securesms.jobs.ForceUpdateGroupV2Job;
|
||||||
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
|
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
|
||||||
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
|
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
|
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
|
||||||
|
@ -594,6 +595,8 @@ public class ConversationParentFragment extends Fragment
|
||||||
.then(GroupV2UpdateSelfProfileKeyJob.withoutLimits(groupId))
|
.then(GroupV2UpdateSelfProfileKeyJob.withoutLimits(groupId))
|
||||||
.enqueue();
|
.enqueue();
|
||||||
|
|
||||||
|
ForceUpdateGroupV2Job.enqueueIfNecessary(groupId);
|
||||||
|
|
||||||
if (viewModel.getArgs().isFirstTimeInSelfCreatedGroup()) {
|
if (viewModel.getArgs().isFirstTimeInSelfCreatedGroup()) {
|
||||||
groupViewModel.inviteFriendsOneTimeIfJustSelfInGroup(getChildFragmentManager(), groupId);
|
groupViewModel.inviteFriendsOneTimeIfJustSelfInGroup(getChildFragmentManager(), groupId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,24 +69,25 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
|
||||||
|
|
||||||
private static final String TAG = Log.tag(GroupDatabase.class);
|
private static final String TAG = Log.tag(GroupDatabase.class);
|
||||||
|
|
||||||
static final String TABLE_NAME = "groups";
|
static final String TABLE_NAME = "groups";
|
||||||
private static final String ID = "_id";
|
private static final String ID = "_id";
|
||||||
static final String GROUP_ID = "group_id";
|
static final String GROUP_ID = "group_id";
|
||||||
public static final String RECIPIENT_ID = "recipient_id";
|
public static final String RECIPIENT_ID = "recipient_id";
|
||||||
private static final String TITLE = "title";
|
private static final String TITLE = "title";
|
||||||
static final String MEMBERS = "members";
|
static final String MEMBERS = "members";
|
||||||
private static final String AVATAR_ID = "avatar_id";
|
private static final String AVATAR_ID = "avatar_id";
|
||||||
private static final String AVATAR_KEY = "avatar_key";
|
private static final String AVATAR_KEY = "avatar_key";
|
||||||
private static final String AVATAR_CONTENT_TYPE = "avatar_content_type";
|
private static final String AVATAR_CONTENT_TYPE = "avatar_content_type";
|
||||||
private static final String AVATAR_RELAY = "avatar_relay";
|
private static final String AVATAR_RELAY = "avatar_relay";
|
||||||
private static final String AVATAR_DIGEST = "avatar_digest";
|
private static final String AVATAR_DIGEST = "avatar_digest";
|
||||||
private static final String TIMESTAMP = "timestamp";
|
private static final String TIMESTAMP = "timestamp";
|
||||||
static final String ACTIVE = "active";
|
static final String ACTIVE = "active";
|
||||||
static final String MMS = "mms";
|
static final String MMS = "mms";
|
||||||
private static final String EXPECTED_V2_ID = "expected_v2_id";
|
private static final String EXPECTED_V2_ID = "expected_v2_id";
|
||||||
private static final String UNMIGRATED_V1_MEMBERS = "former_v1_members";
|
private static final String UNMIGRATED_V1_MEMBERS = "former_v1_members";
|
||||||
private static final String DISTRIBUTION_ID = "distribution_id";
|
private static final String DISTRIBUTION_ID = "distribution_id";
|
||||||
private static final String SHOW_AS_STORY_STATE = "display_as_story";
|
private static final String SHOW_AS_STORY_STATE = "display_as_story";
|
||||||
|
private static final String LAST_FORCE_UPDATE_TIMESTAMP = "last_force_update_timestamp";
|
||||||
|
|
||||||
/** Was temporarily used for PNP accept by pni but is no longer needed/updated */
|
/** Was temporarily used for PNP accept by pni but is no longer needed/updated */
|
||||||
@Deprecated
|
@Deprecated
|
||||||
|
@ -101,27 +102,28 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
|
||||||
/** Serialized {@link DecryptedGroup} protobuf */
|
/** Serialized {@link DecryptedGroup} protobuf */
|
||||||
public static final String V2_DECRYPTED_GROUP = "decrypted_group";
|
public static final String V2_DECRYPTED_GROUP = "decrypted_group";
|
||||||
|
|
||||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||||
GROUP_ID + " TEXT, " +
|
GROUP_ID + " TEXT, " +
|
||||||
RECIPIENT_ID + " INTEGER, " +
|
RECIPIENT_ID + " INTEGER, " +
|
||||||
TITLE + " TEXT, " +
|
TITLE + " TEXT, " +
|
||||||
MEMBERS + " TEXT, " +
|
MEMBERS + " TEXT, " +
|
||||||
AVATAR_ID + " INTEGER, " +
|
AVATAR_ID + " INTEGER, " +
|
||||||
AVATAR_KEY + " BLOB, " +
|
AVATAR_KEY + " BLOB, " +
|
||||||
AVATAR_CONTENT_TYPE + " TEXT, " +
|
AVATAR_CONTENT_TYPE + " TEXT, " +
|
||||||
AVATAR_RELAY + " TEXT, " +
|
AVATAR_RELAY + " TEXT, " +
|
||||||
TIMESTAMP + " INTEGER, " +
|
TIMESTAMP + " INTEGER, " +
|
||||||
ACTIVE + " INTEGER DEFAULT 1, " +
|
ACTIVE + " INTEGER DEFAULT 1, " +
|
||||||
AVATAR_DIGEST + " BLOB, " +
|
AVATAR_DIGEST + " BLOB, " +
|
||||||
MMS + " INTEGER DEFAULT 0, " +
|
MMS + " INTEGER DEFAULT 0, " +
|
||||||
V2_MASTER_KEY + " BLOB, " +
|
V2_MASTER_KEY + " BLOB, " +
|
||||||
V2_REVISION + " BLOB, " +
|
V2_REVISION + " BLOB, " +
|
||||||
V2_DECRYPTED_GROUP + " BLOB, " +
|
V2_DECRYPTED_GROUP + " BLOB, " +
|
||||||
EXPECTED_V2_ID + " TEXT DEFAULT NULL, " +
|
EXPECTED_V2_ID + " TEXT DEFAULT NULL, " +
|
||||||
UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL, " +
|
UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL, " +
|
||||||
DISTRIBUTION_ID + " TEXT DEFAULT NULL, " +
|
DISTRIBUTION_ID + " TEXT DEFAULT NULL, " +
|
||||||
SHOW_AS_STORY_STATE + " INTEGER DEFAULT 0, " +
|
SHOW_AS_STORY_STATE + " INTEGER DEFAULT 0, " +
|
||||||
AUTH_SERVICE_ID + " TEXT DEFAULT NULL);";
|
AUTH_SERVICE_ID + " TEXT DEFAULT NULL, " +
|
||||||
|
LAST_FORCE_UPDATE_TIMESTAMP + " INTEGER DEFAULT 0);";
|
||||||
|
|
||||||
public static final String[] CREATE_INDEXS = {
|
public static final String[] CREATE_INDEXS = {
|
||||||
"CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");",
|
"CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");",
|
||||||
|
@ -132,7 +134,7 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
|
||||||
|
|
||||||
private static final String[] GROUP_PROJECTION = {
|
private static final String[] GROUP_PROJECTION = {
|
||||||
GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, UNMIGRATED_V1_MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
|
GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, UNMIGRATED_V1_MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
|
||||||
TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP
|
TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP, LAST_FORCE_UPDATE_TIMESTAMP
|
||||||
};
|
};
|
||||||
|
|
||||||
static final List<String> TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList();
|
static final List<String> TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList();
|
||||||
|
@ -955,6 +957,12 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
|
||||||
database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId.toString()});
|
database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId.toString()});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setLastForceUpdateTimestamp(@NonNull GroupId groupId, long timestamp) {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(LAST_FORCE_UPDATE_TIMESTAMP, timestamp);
|
||||||
|
getWritableDatabase().update(TABLE_NAME, values, GROUP_ID + " = ?", SqlUtil.buildArgs(groupId));
|
||||||
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public boolean isCurrentMember(@NonNull GroupId.Push groupId, @NonNull RecipientId recipientId) {
|
public boolean isCurrentMember(@NonNull GroupId.Push groupId, @NonNull RecipientId recipientId) {
|
||||||
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
|
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
|
||||||
|
@ -1114,7 +1122,8 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
|
||||||
CursorUtil.requireBlob(cursor, V2_MASTER_KEY),
|
CursorUtil.requireBlob(cursor, V2_MASTER_KEY),
|
||||||
CursorUtil.requireInt(cursor, V2_REVISION),
|
CursorUtil.requireInt(cursor, V2_REVISION),
|
||||||
CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP),
|
CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP),
|
||||||
CursorUtil.getString(cursor, DISTRIBUTION_ID).map(DistributionId::from).orElse(null));
|
CursorUtil.getString(cursor, DISTRIBUTION_ID).map(DistributionId::from).orElse(null),
|
||||||
|
CursorUtil.requireLong(cursor, LAST_FORCE_UPDATE_TIMESTAMP));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1155,6 +1164,7 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
|
||||||
private final boolean mms;
|
private final boolean mms;
|
||||||
@Nullable private final V2GroupProperties v2GroupProperties;
|
@Nullable private final V2GroupProperties v2GroupProperties;
|
||||||
private final DistributionId distributionId;
|
private final DistributionId distributionId;
|
||||||
|
private final long lastForceUpdateTimestamp;
|
||||||
|
|
||||||
public GroupRecord(@NonNull GroupId id,
|
public GroupRecord(@NonNull GroupId id,
|
||||||
@NonNull RecipientId recipientId,
|
@NonNull RecipientId recipientId,
|
||||||
|
@ -1171,19 +1181,21 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
|
||||||
@Nullable byte[] groupMasterKeyBytes,
|
@Nullable byte[] groupMasterKeyBytes,
|
||||||
int groupRevision,
|
int groupRevision,
|
||||||
@Nullable byte[] decryptedGroupBytes,
|
@Nullable byte[] decryptedGroupBytes,
|
||||||
@Nullable DistributionId distributionId)
|
@Nullable DistributionId distributionId,
|
||||||
|
long lastForceUpdateTimestamp)
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.recipientId = recipientId;
|
this.recipientId = recipientId;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.avatarId = avatarId;
|
this.avatarId = avatarId;
|
||||||
this.avatarKey = avatarKey;
|
this.avatarKey = avatarKey;
|
||||||
this.avatarDigest = avatarDigest;
|
this.avatarDigest = avatarDigest;
|
||||||
this.avatarContentType = avatarContentType;
|
this.avatarContentType = avatarContentType;
|
||||||
this.relay = relay;
|
this.relay = relay;
|
||||||
this.active = active;
|
this.active = active;
|
||||||
this.mms = mms;
|
this.mms = mms;
|
||||||
this.distributionId = distributionId;
|
this.distributionId = distributionId;
|
||||||
|
this.lastForceUpdateTimestamp = lastForceUpdateTimestamp;
|
||||||
|
|
||||||
V2GroupProperties v2GroupProperties = null;
|
V2GroupProperties v2GroupProperties = null;
|
||||||
if (groupMasterKeyBytes != null && decryptedGroupBytes != null) {
|
if (groupMasterKeyBytes != null && decryptedGroupBytes != null) {
|
||||||
|
@ -1292,6 +1304,10 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
|
||||||
return distributionId;
|
return distributionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getLastForceUpdateTimestamp() {
|
||||||
|
return lastForceUpdateTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isV1Group() {
|
public boolean isV1Group() {
|
||||||
return !mms && !isV2Group();
|
return !mms && !isV2Group();
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,14 @@ import org.thoughtcrime.securesms.database.helpers.migration.V154_PniSignaturesM
|
||||||
import org.thoughtcrime.securesms.database.helpers.migration.V155_SmsExporterMigration
|
import org.thoughtcrime.securesms.database.helpers.migration.V155_SmsExporterMigration
|
||||||
import org.thoughtcrime.securesms.database.helpers.migration.V156_RecipientUnregisteredTimestampMigration
|
import org.thoughtcrime.securesms.database.helpers.migration.V156_RecipientUnregisteredTimestampMigration
|
||||||
import org.thoughtcrime.securesms.database.helpers.migration.V157_RecipeintHiddenMigration
|
import org.thoughtcrime.securesms.database.helpers.migration.V157_RecipeintHiddenMigration
|
||||||
|
import org.thoughtcrime.securesms.database.helpers.migration.V158_GroupsLastForceUpdateTimestampMigration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||||
*/
|
*/
|
||||||
object SignalDatabaseMigrations {
|
object SignalDatabaseMigrations {
|
||||||
|
|
||||||
const val DATABASE_VERSION = 157
|
const val DATABASE_VERSION = 158
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
@ -57,6 +58,10 @@ object SignalDatabaseMigrations {
|
||||||
if (oldVersion < 157) {
|
if (oldVersion < 157) {
|
||||||
V157_RecipeintHiddenMigration.migrate(context, db, oldVersion, newVersion)
|
V157_RecipeintHiddenMigration.migrate(context, db, oldVersion, newVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < 158) {
|
||||||
|
V158_GroupsLastForceUpdateTimestampMigration.migrate(context, db, oldVersion, newVersion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package org.thoughtcrime.securesms.database.helpers.migration
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track last time we did a forced sanity check for this group with the server.
|
||||||
|
*/
|
||||||
|
object V158_GroupsLastForceUpdateTimestampMigration : SignalDatabaseMigration {
|
||||||
|
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
db.execSQL("ALTER TABLE groups ADD COLUMN last_force_update_timestamp INTEGER DEFAULT 0")
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.signal.core.util.DatabaseId;
|
||||||
import org.signal.core.util.Hex;
|
import org.signal.core.util.Hex;
|
||||||
import org.signal.libsignal.protocol.kdf.HKDFv3;
|
import org.signal.libsignal.protocol.kdf.HKDFv3;
|
||||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||||
|
@ -14,7 +15,7 @@ import org.thoughtcrime.securesms.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
public abstract class GroupId {
|
public abstract class GroupId implements DatabaseId {
|
||||||
|
|
||||||
private static final String ENCODED_SIGNAL_GROUP_V1_PREFIX = "__textsecure_group__!";
|
private static final String ENCODED_SIGNAL_GROUP_V1_PREFIX = "__textsecure_group__!";
|
||||||
private static final String ENCODED_SIGNAL_GROUP_V2_PREFIX = "__signal_group__v2__!";
|
private static final String ENCODED_SIGNAL_GROUP_V2_PREFIX = "__signal_group__v2__!";
|
||||||
|
@ -173,6 +174,11 @@ public abstract class GroupId {
|
||||||
return encodedId;
|
return encodedId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull String serialize() {
|
||||||
|
return encodedId;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract boolean isMms();
|
public abstract boolean isMms();
|
||||||
|
|
||||||
public abstract boolean isV1();
|
public abstract boolean isV1();
|
||||||
|
|
|
@ -188,6 +188,17 @@ public final class GroupManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
public static void forceSanityUpdateFromServer(@NonNull Context context,
|
||||||
|
@NonNull GroupMasterKey groupMasterKey,
|
||||||
|
long timestamp)
|
||||||
|
throws GroupChangeBusyException, IOException, GroupNotAMemberException
|
||||||
|
{
|
||||||
|
try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) {
|
||||||
|
updater.forceSanityUpdateFromServer(timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public static V2GroupServerStatus v2GroupStatus(@NonNull Context context,
|
public static V2GroupServerStatus v2GroupStatus(@NonNull Context context,
|
||||||
@NonNull ServiceId authServiceId,
|
@NonNull ServiceId authServiceId,
|
||||||
|
|
|
@ -798,6 +798,14 @@ final class GroupManagerV2 {
|
||||||
.updateLocalGroupToRevision(revision, timestamp, getDecryptedGroupChange(signedGroupChange));
|
.updateLocalGroupToRevision(revision, timestamp, getDecryptedGroupChange(signedGroupChange));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
void forceSanityUpdateFromServer(long timestamp)
|
||||||
|
throws IOException, GroupNotAMemberException
|
||||||
|
{
|
||||||
|
new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey)
|
||||||
|
.forceSanityUpdateFromServer(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
private DecryptedGroupChange getDecryptedGroupChange(@Nullable byte[] signedGroupChange) {
|
private DecryptedGroupChange getDecryptedGroupChange(@Nullable byte[] signedGroupChange) {
|
||||||
if (signedGroupChange != null) {
|
if (signedGroupChange != null) {
|
||||||
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
|
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
|
||||||
|
@ -928,24 +936,6 @@ final class GroupManagerV2 {
|
||||||
|
|
||||||
if (group.isPresent()) {
|
if (group.isPresent()) {
|
||||||
Log.i(TAG, "Group already present locally");
|
Log.i(TAG, "Group already present locally");
|
||||||
|
|
||||||
DecryptedGroup currentGroupState = group.get()
|
|
||||||
.requireV2GroupProperties()
|
|
||||||
.getDecryptedGroup();
|
|
||||||
|
|
||||||
DecryptedGroup updatedGroup = currentGroupState;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (decryptedChange != null) {
|
|
||||||
updatedGroup = DecryptedGroupUtil.applyWithoutRevisionCheck(updatedGroup, decryptedChange);
|
|
||||||
}
|
|
||||||
updatedGroup = resetRevision(updatedGroup, currentGroupState.getRevision());
|
|
||||||
} catch (NotAbleToApplyGroupV2ChangeException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
updatedGroup = decryptedGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
groupDatabase.update(groupId, updatedGroup);
|
|
||||||
} else {
|
} else {
|
||||||
groupDatabase.create(groupMasterKey, decryptedGroup);
|
groupDatabase.create(groupMasterKey, decryptedGroup);
|
||||||
Log.i(TAG, "Created local group with placeholder");
|
Log.i(TAG, "Created local group with placeholder");
|
||||||
|
|
|
@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage;
|
||||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry;
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage;
|
import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||||
|
@ -170,6 +171,52 @@ public class GroupsV2StateProcessor {
|
||||||
this.profileAndMessageHelper = profileAndMessageHelper;
|
this.profileAndMessageHelper = profileAndMessageHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
public GroupUpdateResult forceSanityUpdateFromServer(long timestamp)
|
||||||
|
throws IOException, GroupNotAMemberException
|
||||||
|
{
|
||||||
|
Optional<GroupRecord> localRecord = groupDatabase.getGroup(groupId);
|
||||||
|
DecryptedGroup localState = localRecord.map(g -> g.requireV2GroupProperties().getDecryptedGroup()).orElse(null);
|
||||||
|
DecryptedGroup serverState;
|
||||||
|
|
||||||
|
if (localState == null) {
|
||||||
|
info("No local state to force update");
|
||||||
|
return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
serverState = groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
|
||||||
|
} catch (NotInGroupException | GroupNotFoundException e) {
|
||||||
|
throw new GroupNotAMemberException(e);
|
||||||
|
} catch (VerificationFailedException | InvalidGroupStateException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(localState, serverState);
|
||||||
|
GlobalGroupState inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(serverState, decryptedGroupChange)));
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(inputGroupState, serverState.getRevision());
|
||||||
|
DecryptedGroup newLocalState = advanceGroupStateResult.getNewGlobalGroupState().getLocalState();
|
||||||
|
|
||||||
|
if (newLocalState == null || newLocalState == inputGroupState.getLocalState()) {
|
||||||
|
info("Local state and server state are equal");
|
||||||
|
return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null);
|
||||||
|
} else {
|
||||||
|
info("Local state (revision: " + localState.getRevision() + ") does not match server state (revision: " + serverState.getRevision() + "), updating");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocalDatabaseGroupState(inputGroupState, newLocalState);
|
||||||
|
if (localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
|
||||||
|
info("Inserting single update message for restore placeholder");
|
||||||
|
profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(newLocalState, null)));
|
||||||
|
} else {
|
||||||
|
info("Inserting force update messages");
|
||||||
|
profileAndMessageHelper.insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries());
|
||||||
|
}
|
||||||
|
profileAndMessageHelper.persistLearnedProfileKeys(inputGroupState);
|
||||||
|
|
||||||
|
return new GroupUpdateResult(GroupState.GROUP_UPDATED, newLocalState);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Using network where required, will attempt to bring the local copy of the group up to the revision specified.
|
* Using network where required, will attempt to bring the local copy of the group up to the revision specified.
|
||||||
*
|
*
|
||||||
|
@ -560,10 +607,12 @@ public class GroupsV2StateProcessor {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final ServiceId serviceId;
|
private final ServiceId serviceId;
|
||||||
private final GroupMasterKey masterKey;
|
|
||||||
private final GroupId.V2 groupId;
|
private final GroupId.V2 groupId;
|
||||||
private final RecipientDatabase recipientDatabase;
|
private final RecipientDatabase recipientDatabase;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
GroupMasterKey masterKey;
|
||||||
|
|
||||||
ProfileAndMessageHelper(@NonNull Context context, @NonNull ServiceId serviceId, @NonNull GroupMasterKey masterKey, @NonNull GroupId.V2 groupId, @NonNull RecipientDatabase recipientDatabase) {
|
ProfileAndMessageHelper(@NonNull Context context, @NonNull ServiceId serviceId, @NonNull GroupMasterKey masterKey, @NonNull GroupId.V2 groupId, @NonNull RecipientDatabase recipientDatabase) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.serviceId = serviceId;
|
this.serviceId = serviceId;
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.signal.core.util.concurrent.SignalExecutors;
|
||||||
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a {@link ForceUpdateGroupV2WorkerJob} to happen after message queues are drained.
|
||||||
|
*/
|
||||||
|
public final class ForceUpdateGroupV2Job extends BaseJob {
|
||||||
|
|
||||||
|
public static final String KEY = "ForceUpdateGroupV2Job";
|
||||||
|
|
||||||
|
private static final long FORCE_UPDATE_INTERVAL = TimeUnit.DAYS.toMillis(7);
|
||||||
|
private static final String KEY_GROUP_ID = "group_id";
|
||||||
|
|
||||||
|
private final GroupId.V2 groupId;
|
||||||
|
|
||||||
|
public static void enqueueIfNecessary(@NonNull GroupId.V2 groupId) {
|
||||||
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
|
Optional<GroupDatabase.GroupRecord> group = SignalDatabase.groups().getGroup(groupId);
|
||||||
|
if (group.isPresent() &&
|
||||||
|
group.get().isV2Group() &&
|
||||||
|
group.get().getLastForceUpdateTimestamp() + FORCE_UPDATE_INTERVAL < System.currentTimeMillis()
|
||||||
|
) {
|
||||||
|
ApplicationDependencies.getJobManager().add(new ForceUpdateGroupV2Job(groupId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private ForceUpdateGroupV2Job(@NonNull GroupId.V2 groupId) {
|
||||||
|
this(new Parameters.Builder().setQueue("ForceUpdateGroupV2Job_" + groupId)
|
||||||
|
.setMaxInstancesForQueue(1)
|
||||||
|
.addConstraint(DecryptionsDrainedConstraint.KEY)
|
||||||
|
.setMaxAttempts(Parameters.UNLIMITED)
|
||||||
|
.build(),
|
||||||
|
groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ForceUpdateGroupV2Job(@NonNull Parameters parameters, @NonNull GroupId.V2 groupId) {
|
||||||
|
super(parameters);
|
||||||
|
this.groupId = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull Data serialize() {
|
||||||
|
return new Data.Builder().putString(KEY_GROUP_ID, groupId.toString()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull String getFactoryKey() {
|
||||||
|
return KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRun() {
|
||||||
|
ApplicationDependencies.getJobManager().add(new ForceUpdateGroupV2WorkerJob(groupId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onShouldRetry(@NonNull Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Factory implements Job.Factory<ForceUpdateGroupV2Job> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull ForceUpdateGroupV2Job create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
return new ForceUpdateGroupV2Job(parameters, GroupId.parseOrThrow(data.getString(KEY_GROUP_ID)).requireV2());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
|
import org.signal.core.util.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
||||||
|
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scheduled by {@link ForceUpdateGroupV2Job} after message queues are drained.
|
||||||
|
*
|
||||||
|
* Forces a sanity check between local state and server state, and updates local state
|
||||||
|
* as necessary.
|
||||||
|
*/
|
||||||
|
final class ForceUpdateGroupV2WorkerJob extends BaseJob {
|
||||||
|
|
||||||
|
public static final String KEY = "ForceUpdateGroupV2WorkerJob";
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(ForceUpdateGroupV2WorkerJob.class);
|
||||||
|
|
||||||
|
private static final String KEY_GROUP_ID = "group_id";
|
||||||
|
|
||||||
|
private final GroupId.V2 groupId;
|
||||||
|
|
||||||
|
ForceUpdateGroupV2WorkerJob(@NonNull GroupId.V2 groupId) {
|
||||||
|
this(new Parameters.Builder().setQueue(PushProcessMessageJob.getQueueName(Recipient.externalGroupExact(groupId).getId()))
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setMaxAttempts(Parameters.UNLIMITED)
|
||||||
|
.build(),
|
||||||
|
groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ForceUpdateGroupV2WorkerJob(@NonNull Parameters parameters, @NonNull GroupId.V2 groupId) {
|
||||||
|
super(parameters);
|
||||||
|
this.groupId = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull Data serialize() {
|
||||||
|
return new Data.Builder().putString(KEY_GROUP_ID, groupId.toString())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull String getFactoryKey() {
|
||||||
|
return KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRun() throws IOException, GroupNotAMemberException, GroupChangeBusyException {
|
||||||
|
Optional<GroupDatabase.GroupRecord> group = SignalDatabase.groups().getGroup(groupId);
|
||||||
|
|
||||||
|
if (!group.isPresent()) {
|
||||||
|
Log.w(TAG, "Group not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Recipient.externalGroupExact(groupId).isBlocked()) {
|
||||||
|
Log.i(TAG, "Not fetching group info for blocked group " + groupId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupManager.forceSanityUpdateFromServer(context, group.get().requireV2GroupProperties().getGroupMasterKey(), System.currentTimeMillis());
|
||||||
|
|
||||||
|
SignalDatabase.groups().setLastForceUpdateTimestamp(group.get().getId(), System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onShouldRetry(@NonNull Exception e) {
|
||||||
|
return e instanceof PushNetworkException ||
|
||||||
|
e instanceof NoCredentialForRedemptionTimeException ||
|
||||||
|
e instanceof GroupChangeBusyException;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Factory implements Job.Factory<ForceUpdateGroupV2WorkerJob> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull ForceUpdateGroupV2WorkerJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
return new ForceUpdateGroupV2WorkerJob(parameters,
|
||||||
|
GroupId.parseOrThrow(data.getString(KEY_GROUP_ID)).requireV2());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -97,6 +97,8 @@ public final class JobManagerFactories {
|
||||||
put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory());
|
put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory());
|
||||||
put(FetchRemoteMegaphoneImageJob.KEY, new FetchRemoteMegaphoneImageJob.Factory());
|
put(FetchRemoteMegaphoneImageJob.KEY, new FetchRemoteMegaphoneImageJob.Factory());
|
||||||
put(FontDownloaderJob.KEY, new FontDownloaderJob.Factory());
|
put(FontDownloaderJob.KEY, new FontDownloaderJob.Factory());
|
||||||
|
put(ForceUpdateGroupV2Job.KEY, new ForceUpdateGroupV2Job.Factory());
|
||||||
|
put(ForceUpdateGroupV2WorkerJob.KEY, new ForceUpdateGroupV2WorkerJob.Factory());
|
||||||
put(GiftSendJob.KEY, new GiftSendJob.Factory());
|
put(GiftSendJob.KEY, new GiftSendJob.Factory());
|
||||||
put(GroupV1MigrationJob.KEY, new GroupV1MigrationJob.Factory());
|
put(GroupV1MigrationJob.KEY, new GroupV1MigrationJob.Factory());
|
||||||
put(GroupCallUpdateSendJob.KEY, new GroupCallUpdateSendJob.Factory());
|
put(GroupCallUpdateSendJob.KEY, new GroupCallUpdateSendJob.Factory());
|
||||||
|
@ -136,7 +138,7 @@ public final class JobManagerFactories {
|
||||||
put(PaymentNotificationSendJob.KEY, new PaymentNotificationSendJob.Factory());
|
put(PaymentNotificationSendJob.KEY, new PaymentNotificationSendJob.Factory());
|
||||||
put(PaymentSendJob.KEY, new PaymentSendJob.Factory());
|
put(PaymentSendJob.KEY, new PaymentSendJob.Factory());
|
||||||
put(PaymentTransactionCheckJob.KEY, new PaymentTransactionCheckJob.Factory());
|
put(PaymentTransactionCheckJob.KEY, new PaymentTransactionCheckJob.Factory());
|
||||||
put(PnpInitializeDevicesJob.KEY, new PnpInitializeDevicesJob.Factory());
|
put(PnpInitializeDevicesJob.KEY, new PnpInitializeDevicesJob.Factory());
|
||||||
put(PreKeysSyncJob.KEY, new PreKeysSyncJob.Factory());
|
put(PreKeysSyncJob.KEY, new PreKeysSyncJob.Factory());
|
||||||
put(ProfileKeySendJob.KEY, new ProfileKeySendJob.Factory());
|
put(ProfileKeySendJob.KEY, new ProfileKeySendJob.Factory());
|
||||||
put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory());
|
put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory());
|
||||||
|
|
|
@ -190,7 +190,8 @@ fun groupRecord(
|
||||||
masterKey.serialize(),
|
masterKey.serialize(),
|
||||||
decryptedGroup.revision,
|
decryptedGroup.revision,
|
||||||
decryptedGroup.toByteArray(),
|
decryptedGroup.toByteArray(),
|
||||||
distributionId
|
distributionId,
|
||||||
|
System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,19 +3,28 @@ package org.thoughtcrime.securesms.groups.v2.processing
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import org.hamcrest.MatcherAssert.assertThat
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.hamcrest.Matchers.both
|
||||||
|
import org.hamcrest.Matchers.hasItem
|
||||||
|
import org.hamcrest.Matchers.hasProperty
|
||||||
import org.hamcrest.Matchers.`is`
|
import org.hamcrest.Matchers.`is`
|
||||||
|
import org.hamcrest.Matchers.notNullValue
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.ArgumentCaptor
|
||||||
|
import org.mockito.ArgumentMatchers.anyLong
|
||||||
import org.mockito.ArgumentMatchers.eq
|
import org.mockito.ArgumentMatchers.eq
|
||||||
import org.mockito.Mockito.any
|
import org.mockito.Mockito.any
|
||||||
|
import org.mockito.Mockito.doCallRealMethod
|
||||||
import org.mockito.Mockito.doReturn
|
import org.mockito.Mockito.doReturn
|
||||||
import org.mockito.Mockito.isA
|
import org.mockito.Mockito.isA
|
||||||
import org.mockito.Mockito.mock
|
import org.mockito.Mockito.mock
|
||||||
import org.mockito.Mockito.reset
|
import org.mockito.Mockito.reset
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.kotlin.anyOrNull
|
||||||
|
import org.mockito.kotlin.doNothing
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import org.signal.core.util.Hex.fromStringCondensed
|
import org.signal.core.util.Hex.fromStringCondensed
|
||||||
|
@ -25,11 +34,13 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedString
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer
|
import org.signal.storageservice.protos.groups.local.DecryptedTimer
|
||||||
import org.thoughtcrime.securesms.SignalStoreRule
|
import org.thoughtcrime.securesms.SignalStoreRule
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||||
import org.thoughtcrime.securesms.database.GroupStateTestData
|
import org.thoughtcrime.securesms.database.GroupStateTestData
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.member
|
import org.thoughtcrime.securesms.database.model.databaseprotos.member
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.requestingMember
|
import org.thoughtcrime.securesms.database.model.databaseprotos.requestingMember
|
||||||
import org.thoughtcrime.securesms.database.setNewDescription
|
import org.thoughtcrime.securesms.database.setNewDescription
|
||||||
|
@ -104,6 +115,7 @@ class GroupsV2StateProcessorTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
doReturn(testPartial).`when`(groupsV2API).getPartialDecryptedGroup(any(), any())
|
doReturn(testPartial).`when`(groupsV2API).getPartialDecryptedGroup(any(), any())
|
||||||
|
doReturn(serverState).`when`(groupsV2API).getGroup(any(), any())
|
||||||
}
|
}
|
||||||
|
|
||||||
data.changeSet?.let { changeSet ->
|
data.changeSet?.let { changeSet ->
|
||||||
|
@ -453,4 +465,118 @@ class GroupsV2StateProcessorTest {
|
||||||
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||||
assertThat("revision matches latest revision on server", result.latestServer!!.revision, `is`(101))
|
assertThat("revision matches latest revision on server", result.latestServer!!.revision, `is`(101))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If for some reason we missed a member being added in our local state, and then we preform a multi-revision update,
|
||||||
|
* we should now know about the member and add update messages to the chat.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun missedMemberAddResolvesWithMultipleRevisionUpdate() {
|
||||||
|
val secondOther = member(ServiceId.from(UUID.randomUUID()))
|
||||||
|
|
||||||
|
val updateMessageContextCapture = ArgumentCaptor.forClass(DecryptedGroupV2Context::class.java)
|
||||||
|
profileAndMessageHelper.masterKey = masterKey
|
||||||
|
doCallRealMethod().`when`(profileAndMessageHelper).insertUpdateMessages(anyLong(), anyOrNull(), any())
|
||||||
|
doNothing().`when`(profileAndMessageHelper).storeMessage(updateMessageContextCapture.capture(), anyLong())
|
||||||
|
|
||||||
|
given {
|
||||||
|
localState(
|
||||||
|
revision = 8,
|
||||||
|
title = "Whatever",
|
||||||
|
members = selfAndOthers
|
||||||
|
)
|
||||||
|
serverState(
|
||||||
|
revision = 10,
|
||||||
|
title = "Changed",
|
||||||
|
members = selfAndOthers + secondOther
|
||||||
|
)
|
||||||
|
changeSet {
|
||||||
|
changeLog(9) {
|
||||||
|
change {
|
||||||
|
setNewTitle("Mid-Change")
|
||||||
|
}
|
||||||
|
fullSnapshot(
|
||||||
|
title = "Mid-Change",
|
||||||
|
members = selfAndOthers + secondOther
|
||||||
|
)
|
||||||
|
}
|
||||||
|
changeLog(10) {
|
||||||
|
change {
|
||||||
|
setNewTitle("Changed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiCallParameters(requestedRevision = 8, includeFirst = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
||||||
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||||
|
assertThat("members contains second other", result.latestServer!!.membersList, hasItem(secondOther))
|
||||||
|
|
||||||
|
val allUpdateMessageContexts = updateMessageContextCapture.allValues
|
||||||
|
assertThat("group update messages contains new member add", allUpdateMessageContexts.map { it.change.newMembersList }, hasItem(hasItem(secondOther)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If for some reason we missed a member being added in our local state, and then we preform a forced sanity update,
|
||||||
|
* we should now know about the member and any other changes, and add update messages to the chat.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun missedMemberAddResolvesWithForcedUpdate() {
|
||||||
|
val secondOther = member(ServiceId.from(UUID.randomUUID()))
|
||||||
|
|
||||||
|
val updateMessageContextCapture = ArgumentCaptor.forClass(DecryptedGroupV2Context::class.java)
|
||||||
|
profileAndMessageHelper.masterKey = masterKey
|
||||||
|
doCallRealMethod().`when`(profileAndMessageHelper).insertUpdateMessages(anyLong(), anyOrNull(), any())
|
||||||
|
doNothing().`when`(profileAndMessageHelper).storeMessage(updateMessageContextCapture.capture(), anyLong())
|
||||||
|
|
||||||
|
given {
|
||||||
|
localState(
|
||||||
|
revision = 10,
|
||||||
|
title = "Title",
|
||||||
|
members = selfAndOthers
|
||||||
|
)
|
||||||
|
serverState(
|
||||||
|
revision = 10,
|
||||||
|
title = "Changed",
|
||||||
|
members = selfAndOthers + secondOther
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = processor.forceSanityUpdateFromServer(0)
|
||||||
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||||
|
assertThat("members contains second other", result.latestServer!!.membersList, hasItem(secondOther))
|
||||||
|
assertThat("title should be updated", result.latestServer!!.title, `is`("Changed"))
|
||||||
|
|
||||||
|
val allUpdateMessageContexts = updateMessageContextCapture.allValues
|
||||||
|
assertThat("group update messages contains new member add", allUpdateMessageContexts.map { it.change.newMembersList }, hasItem(hasItem(secondOther)))
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
"group update messages contains title change",
|
||||||
|
allUpdateMessageContexts.map { it.change.newTitle },
|
||||||
|
hasItem(both<DecryptedString>(notNullValue()).and(hasProperty("value", `is`("Changed"))))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we preform a forced sanity update, with no differences between local and server, then it should be no-op.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun noDifferencesNoOpsWithForcedUpdate() {
|
||||||
|
given {
|
||||||
|
localState(
|
||||||
|
revision = 10,
|
||||||
|
title = "Title",
|
||||||
|
members = selfAndOthers
|
||||||
|
)
|
||||||
|
serverState(
|
||||||
|
revision = 10,
|
||||||
|
title = "Title",
|
||||||
|
members = selfAndOthers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = processor.forceSanityUpdateFromServer(0)
|
||||||
|
assertThat("local should be unchanged", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_CONSISTENT_OR_AHEAD))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue