Add the ability to migrate GV1 groups to GV2.
Co-authored-by: Alan Evans <alan@signal.org>
This commit is contained in:
parent
2d1bf33902
commit
6bb9d27d4e
34 changed files with 818 additions and 132 deletions
|
@ -1,5 +1,6 @@
|
||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||||
|
|
||||||
public final class AppCapabilities {
|
public final class AppCapabilities {
|
||||||
|
@ -7,15 +8,14 @@ public final class AppCapabilities {
|
||||||
private AppCapabilities() {
|
private AppCapabilities() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final boolean UUID_CAPABLE = false;
|
private static final boolean UUID_CAPABLE = false;
|
||||||
private static final boolean GV2_CAPABLE = true;
|
private static final boolean GV2_CAPABLE = true;
|
||||||
private static final boolean GV1_MIGRATION_CAPABLE = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param storageCapable Whether or not the user can use storage service. This is another way of
|
* @param storageCapable Whether or not the user can use storage service. This is another way of
|
||||||
* asking if the user has set a Signal PIN or not.
|
* asking if the user has set a Signal PIN or not.
|
||||||
*/
|
*/
|
||||||
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
||||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION_CAPABLE);
|
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, FeatureFlags.groupsV1AutoMigration());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||||
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
||||||
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
||||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||||
|
@ -155,6 +156,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||||
FeatureFlags.refreshIfNecessary();
|
FeatureFlags.refreshIfNecessary();
|
||||||
ApplicationDependencies.getRecipientCache().warmUp();
|
ApplicationDependencies.getRecipientCache().warmUp();
|
||||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||||
|
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(this);
|
||||||
executePendingContactSync();
|
executePendingContactSync();
|
||||||
KeyCachingService.onAppForegrounded(this);
|
KeyCachingService.onAppForegrounded(this);
|
||||||
ApplicationDependencies.getFrameRateTracker().begin();
|
ApplicationDependencies.getFrameRateTracker().begin();
|
||||||
|
|
|
@ -167,6 +167,7 @@ import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
||||||
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.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;
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||||
|
@ -451,6 +452,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Boolean result) {
|
public void onSuccess(Boolean result) {
|
||||||
initializeProfiles();
|
initializeProfiles();
|
||||||
|
initializeGv1Migration();
|
||||||
initializeDraft().addListener(new AssertedSuccessListener<Boolean>() {
|
initializeDraft().addListener(new AssertedSuccessListener<Boolean>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Boolean loadedDraft) {
|
public void onSuccess(Boolean loadedDraft) {
|
||||||
|
@ -2123,6 +2125,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||||
RetrieveProfileJob.enqueueAsync(recipient.getId());
|
RetrieveProfileJob.enqueueAsync(recipient.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initializeGv1Migration() {
|
||||||
|
GroupV1MigrationJob.enqueuePossibleAutoMigrate(recipient.getId());
|
||||||
|
}
|
||||||
|
|
||||||
private void onRecipientChanged(@NonNull Recipient recipient) {
|
private void onRecipientChanged(@NonNull Recipient recipient) {
|
||||||
Log.i(TAG, "onModified(" + recipient.getId() + ") " + recipient.getRegistered());
|
Log.i(TAG, "onModified(" + recipient.getId() + ") " + recipient.getRegistered());
|
||||||
titleView.setTitle(glideRequests, recipient);
|
titleView.setTitle(glideRequests, recipient);
|
||||||
|
|
|
@ -468,6 +468,48 @@ public final class GroupDatabase extends Database {
|
||||||
notifyConversationListListeners();
|
notifyConversationListListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates a V1 group to a V2 group.
|
||||||
|
*/
|
||||||
|
public @NonNull GroupId.V2 migrateToV2(@NonNull GroupId.V1 groupIdV1, @NonNull DecryptedGroup decryptedGroup) {
|
||||||
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
GroupId.V2 groupIdV2 = groupIdV1.deriveV2MigrationGroupId();
|
||||||
|
GroupMasterKey groupMasterKey = groupIdV1.deriveV2MigrationMasterKey();
|
||||||
|
|
||||||
|
db.beginTransaction();
|
||||||
|
try {
|
||||||
|
GroupRecord record = getGroup(groupIdV1).get();
|
||||||
|
|
||||||
|
ContentValues contentValues = new ContentValues();
|
||||||
|
contentValues.put(GROUP_ID, groupIdV2.toString());
|
||||||
|
contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize());
|
||||||
|
contentValues.putNull(EXPECTED_V2_ID);
|
||||||
|
|
||||||
|
List<RecipientId> newMembers = Stream.of(DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList())).map(u -> RecipientId.from(u, null)).toList();
|
||||||
|
newMembers.addAll(Stream.of(DecryptedGroupUtil.pendingToUuidList(decryptedGroup.getPendingMembersList())).map(u -> RecipientId.from(u, null)).toList());
|
||||||
|
|
||||||
|
if (record.getMembers().size() > newMembers.size() || !newMembers.containsAll(record.getMembers())) {
|
||||||
|
contentValues.put(FORMER_V1_MEMBERS, RecipientId.toSerializedList(record.getMembers()));
|
||||||
|
}
|
||||||
|
|
||||||
|
int updated = db.update(TABLE_NAME, contentValues, GROUP_ID + " = ?", SqlUtil.buildArgs(groupIdV1.toString()));
|
||||||
|
|
||||||
|
if (updated != 1) {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseFactory.getRecipientDatabase(context).updateGroupId(groupIdV1, groupIdV2);
|
||||||
|
|
||||||
|
update(groupMasterKey, decryptedGroup);
|
||||||
|
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
} finally {
|
||||||
|
db.endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupIdV2;
|
||||||
|
}
|
||||||
|
|
||||||
public void update(@NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup decryptedGroup) {
|
public void update(@NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup decryptedGroup) {
|
||||||
update(GroupId.v2(groupMasterKey), decryptedGroup);
|
update(GroupId.v2(groupMasterKey), decryptedGroup);
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,6 +137,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||||
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
|
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
|
||||||
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, int defaultReceiptStatus, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
|
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, int defaultReceiptStatus, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
|
||||||
public abstract void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName);
|
public abstract void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName);
|
||||||
|
public abstract void insertGroupV1MigrationEvent(@NonNull RecipientId recipientId, long threadId, List<RecipientId> pendingRecipients);
|
||||||
|
|
||||||
public abstract boolean deleteMessage(long messageId);
|
public abstract boolean deleteMessage(long messageId);
|
||||||
abstract void deleteThread(long threadId);
|
abstract void deleteThread(long threadId);
|
||||||
|
|
|
@ -414,6 +414,11 @@ public class MmsDatabase extends MessageDatabase {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertGroupV1MigrationEvent(@NonNull RecipientId recipientId, long threadId, List<RecipientId> pendingRecipients) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void endTransaction(SQLiteDatabase database) {
|
public void endTransaction(SQLiteDatabase database) {
|
||||||
database.endTransaction();
|
database.endTransaction();
|
||||||
|
@ -588,7 +593,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||||
|
|
||||||
private long getThreadIdFor(@NonNull IncomingMediaMessage retrieved) {
|
private long getThreadIdFor(@NonNull IncomingMediaMessage retrieved) {
|
||||||
if (retrieved.getGroupId() != null) {
|
if (retrieved.getGroupId() != null) {
|
||||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(retrieved.getGroupId());
|
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromPossiblyMigratedGroupId(retrieved.getGroupId());
|
||||||
Recipient groupRecipients = Recipient.resolved(groupRecipientId);
|
Recipient groupRecipients = Recipient.resolved(groupRecipientId);
|
||||||
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients);
|
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -40,6 +40,7 @@ public interface MmsSmsColumns {
|
||||||
protected static final long INVALID_MESSAGE_TYPE = 6;
|
protected static final long INVALID_MESSAGE_TYPE = 6;
|
||||||
protected static final long PROFILE_CHANGE_TYPE = 7;
|
protected static final long PROFILE_CHANGE_TYPE = 7;
|
||||||
protected static final long MISSED_VIDEO_CALL_TYPE = 8;
|
protected static final long MISSED_VIDEO_CALL_TYPE = 8;
|
||||||
|
protected static final long GV1_MIGRATION_TYPE = 9;
|
||||||
|
|
||||||
protected static final long BASE_INBOX_TYPE = 20;
|
protected static final long BASE_INBOX_TYPE = 20;
|
||||||
protected static final long BASE_OUTBOX_TYPE = 21;
|
protected static final long BASE_OUTBOX_TYPE = 21;
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||||
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||||
|
@ -377,8 +378,8 @@ public class RecipientDatabase extends Database {
|
||||||
return getByColumn(EMAIL, email);
|
return getByColumn(EMAIL, email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull Optional<RecipientId> getByGroupId(@NonNull String groupId) {
|
public @NonNull Optional<RecipientId> getByGroupId(@NonNull GroupId groupId) {
|
||||||
return getByColumn(GROUP_ID, groupId);
|
return getByColumn(GROUP_ID, groupId.toString());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -554,7 +555,7 @@ public class RecipientDatabase extends Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull RecipientId getOrInsertFromGroupId(@NonNull GroupId groupId) {
|
public @NonNull RecipientId getOrInsertFromGroupId(@NonNull GroupId groupId) {
|
||||||
Optional<RecipientId> existing = getByColumn(GROUP_ID, groupId.toString());
|
Optional<RecipientId> existing = getByGroupId(groupId);
|
||||||
|
|
||||||
if (existing.isPresent()) {
|
if (existing.isPresent()) {
|
||||||
return existing.get();
|
return existing.get();
|
||||||
|
@ -604,6 +605,44 @@ public class RecipientDatabase extends Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link Recipient#externalPossiblyMigratedGroup(Context, GroupId)}.
|
||||||
|
*/
|
||||||
|
public @NonNull RecipientId getOrInsertFromPossiblyMigratedGroupId(@NonNull GroupId groupId) {
|
||||||
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
|
||||||
|
db.beginTransaction();
|
||||||
|
try {
|
||||||
|
Optional<RecipientId> existing = getByColumn(GROUP_ID, groupId.toString());
|
||||||
|
|
||||||
|
if (existing.isPresent()) {
|
||||||
|
return existing.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupId.isV1()) {
|
||||||
|
Optional<RecipientId> v2 = getByGroupId(groupId.requireV1().deriveV2MigrationGroupId());
|
||||||
|
if (v2.isPresent()) {
|
||||||
|
return v2.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupId.isV2()) {
|
||||||
|
Optional<GroupDatabase.GroupRecord> v1 = DatabaseFactory.getGroupDatabase(context).getGroupV1ByExpectedV2(groupId.requireV2());
|
||||||
|
if (v1.isPresent()) {
|
||||||
|
return v1.get().getRecipientId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RecipientId id = getOrInsertFromGroupId(groupId);
|
||||||
|
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
|
||||||
|
return id;
|
||||||
|
} finally {
|
||||||
|
db.endTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Cursor getBlocked() {
|
public Cursor getBlocked() {
|
||||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||||
|
|
||||||
|
@ -877,7 +916,7 @@ public class RecipientDatabase extends Database {
|
||||||
for (SignalGroupV1Record insert : groupV1Inserts) {
|
for (SignalGroupV1Record insert : groupV1Inserts) {
|
||||||
db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert));
|
db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert));
|
||||||
|
|
||||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v1orThrow(insert.getGroupId()));
|
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(insert.getGroupId()));
|
||||||
|
|
||||||
threadDatabase.applyStorageSyncUpdate(recipient.getId(), insert);
|
threadDatabase.applyStorageSyncUpdate(recipient.getId(), insert);
|
||||||
needsRefresh.add(recipient.getId());
|
needsRefresh.add(recipient.getId());
|
||||||
|
@ -891,7 +930,7 @@ public class RecipientDatabase extends Database {
|
||||||
throw new AssertionError("Had an update, but it didn't match any rows!");
|
throw new AssertionError("Had an update, but it didn't match any rows!");
|
||||||
}
|
}
|
||||||
|
|
||||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v1orThrow(update.getOld().getGroupId()));
|
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(update.getOld().getGroupId()));
|
||||||
|
|
||||||
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
|
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
|
||||||
needsRefresh.add(recipient.getId());
|
needsRefresh.add(recipient.getId());
|
||||||
|
@ -902,7 +941,7 @@ public class RecipientDatabase extends Database {
|
||||||
GroupId.V2 groupId = GroupId.v2(masterKey);
|
GroupId.V2 groupId = GroupId.v2(masterKey);
|
||||||
ContentValues values = getValuesForStorageGroupV2(insert);
|
ContentValues values = getValuesForStorageGroupV2(insert);
|
||||||
long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
||||||
Recipient recipient = Recipient.externalGroup(context, groupId);
|
Recipient recipient = Recipient.externalGroupExact(context, groupId);
|
||||||
|
|
||||||
if (id < 0) {
|
if (id < 0) {
|
||||||
Log.w(TAG, String.format("Recipient %s is already linked to group %s", recipient.getId(), groupId));
|
Log.w(TAG, String.format("Recipient %s is already linked to group %s", recipient.getId(), groupId));
|
||||||
|
@ -934,7 +973,7 @@ public class RecipientDatabase extends Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupMasterKey masterKey = update.getOld().getMasterKeyOrThrow();
|
GroupMasterKey masterKey = update.getOld().getMasterKeyOrThrow();
|
||||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v2(masterKey));
|
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v2(masterKey));
|
||||||
|
|
||||||
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
|
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
|
||||||
needsRefresh.add(recipient.getId());
|
needsRefresh.add(recipient.getId());
|
||||||
|
@ -1155,7 +1194,7 @@ public class RecipientDatabase extends Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (GroupId.V2 id : DatabaseFactory.getGroupDatabase(context).getAllGroupV2Ids()) {
|
for (GroupId.V2 id : DatabaseFactory.getGroupDatabase(context).getAllGroupV2Ids()) {
|
||||||
Recipient recipient = Recipient.externalGroup(context, id);
|
Recipient recipient = Recipient.externalGroupExact(context, id);
|
||||||
RecipientId recipientId = recipient.getId();
|
RecipientId recipientId = recipient.getId();
|
||||||
RecipientSettings recipientSettingsForSync = getRecipientSettingsForSync(recipientId);
|
RecipientSettings recipientSettingsForSync = getRecipientSettingsForSync(recipientId);
|
||||||
|
|
||||||
|
@ -2300,6 +2339,24 @@ public class RecipientDatabase extends Database {
|
||||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, query, args);
|
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, query, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a group recipient with a new V2 group ID. Should only be done as a part of GV1->GV2
|
||||||
|
* migration.
|
||||||
|
*/
|
||||||
|
void updateGroupId(@NonNull GroupId.V1 v1Id, @NonNull GroupId.V2 v2Id) {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(GROUP_ID, v2Id.toString());
|
||||||
|
values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
|
||||||
|
|
||||||
|
SqlUtil.Query query = SqlUtil.buildTrueUpdateQuery(GROUP_ID + " = ?", SqlUtil.buildArgs(v1Id), values);
|
||||||
|
|
||||||
|
if (update(query, values)) {
|
||||||
|
RecipientId id = getByGroupId(v2Id).get();
|
||||||
|
markDirty(id, DirtyState.UPDATE);
|
||||||
|
Recipient.live(id).refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will update the database with the content values you specified. It will make an intelligent
|
* Will update the database with the content values you specified. It will make an intelligent
|
||||||
* query such that this will only return true if a row was *actually* updated.
|
* query such that this will only return true if a row was *actually* updated.
|
||||||
|
|
|
@ -739,6 +739,24 @@ public class SmsDatabase extends MessageDatabase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertGroupV1MigrationEvent(@NonNull RecipientId recipientId, long threadId, List<RecipientId> pendingRecipients) {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(RECIPIENT_ID, recipientId.serialize());
|
||||||
|
values.put(ADDRESS_DEVICE_ID, 1);
|
||||||
|
values.put(DATE_RECEIVED, System.currentTimeMillis());
|
||||||
|
values.put(DATE_SENT, System.currentTimeMillis());
|
||||||
|
values.put(READ, 1);
|
||||||
|
values.put(TYPE, Types.GV1_MIGRATION_TYPE);
|
||||||
|
values.put(THREAD_ID, threadId);
|
||||||
|
values.put(BODY, RecipientId.toSerializedList(pendingRecipients));
|
||||||
|
|
||||||
|
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values);
|
||||||
|
|
||||||
|
notifyConversationListeners(threadId);
|
||||||
|
ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type) {
|
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type) {
|
||||||
if (message.isJoined()) {
|
if (message.isJoined()) {
|
||||||
|
@ -775,7 +793,7 @@ public class SmsDatabase extends MessageDatabase {
|
||||||
if (message.getGroupId() == null) {
|
if (message.getGroupId() == null) {
|
||||||
groupRecipient = null;
|
groupRecipient = null;
|
||||||
} else {
|
} else {
|
||||||
RecipientId id = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(message.getGroupId());
|
RecipientId id = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromPossiblyMigratedGroupId(message.getGroupId());
|
||||||
groupRecipient = Recipient.resolved(id);
|
groupRecipient = Recipient.resolved(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,6 @@ import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -577,6 +576,28 @@ public class ThreadDatabase extends Database {
|
||||||
return db.rawQuery(query, null);
|
return db.rawQuery(query, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @NonNull List<ThreadRecord> getRecentV1Groups(int limit) {
|
||||||
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
|
String where = MESSAGE_COUNT + " != 0 AND " +
|
||||||
|
"(" +
|
||||||
|
GroupDatabase.TABLE_NAME + "." + GroupDatabase.ACTIVE + " = 1 AND " +
|
||||||
|
GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY + " IS NULL AND " +
|
||||||
|
GroupDatabase.TABLE_NAME + "." + GroupDatabase.MMS + " = 0" +
|
||||||
|
")";
|
||||||
|
String query = createQuery(where, 0, limit, true);
|
||||||
|
|
||||||
|
List<ThreadRecord> threadRecords = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Reader reader = readerFor(db.rawQuery(query, null))) {
|
||||||
|
ThreadRecord record;
|
||||||
|
|
||||||
|
while ((record = reader.getNext()) != null) {
|
||||||
|
threadRecords.add(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return threadRecords;
|
||||||
|
}
|
||||||
|
|
||||||
public Cursor getConversationList() {
|
public Cursor getConversationList() {
|
||||||
return getConversationList("0");
|
return getConversationList("0");
|
||||||
}
|
}
|
||||||
|
@ -697,7 +718,7 @@ public class ThreadDatabase extends Database {
|
||||||
final String query;
|
final String query;
|
||||||
|
|
||||||
if (pinned) {
|
if (pinned) {
|
||||||
query = createQuery(where, PINNED + " ASC", offset, limit, false);
|
query = createQuery(where, PINNED + " ASC", offset, limit);
|
||||||
} else {
|
} else {
|
||||||
query = createQuery(where, offset, limit, false);
|
query = createQuery(where, offset, limit, false);
|
||||||
}
|
}
|
||||||
|
@ -1076,14 +1097,14 @@ public class ThreadDatabase extends Database {
|
||||||
pinnedRecipient = Recipient.externalPush(context, pinned.getContact().get());
|
pinnedRecipient = Recipient.externalPush(context, pinned.getContact().get());
|
||||||
} else if (pinned.getGroupV1Id().isPresent()) {
|
} else if (pinned.getGroupV1Id().isPresent()) {
|
||||||
try {
|
try {
|
||||||
pinnedRecipient = Recipient.externalGroup(context, GroupId.v1Exact(pinned.getGroupV1Id().get()));
|
pinnedRecipient = Recipient.externalGroupExact(context, GroupId.v1Exact(pinned.getGroupV1Id().get()));
|
||||||
} catch (BadGroupIdException e) {
|
} catch (BadGroupIdException e) {
|
||||||
Log.w(TAG, "Failed to parse pinned groupV1 ID!", e);
|
Log.w(TAG, "Failed to parse pinned groupV1 ID!", e);
|
||||||
pinnedRecipient = null;
|
pinnedRecipient = null;
|
||||||
}
|
}
|
||||||
} else if (pinned.getGroupV2MasterKey().isPresent()) {
|
} else if (pinned.getGroupV2MasterKey().isPresent()) {
|
||||||
try {
|
try {
|
||||||
pinnedRecipient = Recipient.externalGroup(context, GroupId.v2(new GroupMasterKey(pinned.getGroupV2MasterKey().get())));
|
pinnedRecipient = Recipient.externalGroupExact(context, GroupId.v2(new GroupMasterKey(pinned.getGroupV2MasterKey().get())));
|
||||||
} catch (InvalidInputException e) {
|
} catch (InvalidInputException e) {
|
||||||
Log.w(TAG, "Failed to parse pinned groupV2 master key!", e);
|
Log.w(TAG, "Failed to parse pinned groupV2 master key!", e);
|
||||||
pinnedRecipient = null;
|
pinnedRecipient = null;
|
||||||
|
@ -1321,10 +1342,10 @@ public class ThreadDatabase extends Database {
|
||||||
private @NonNull String createQuery(@NonNull String where, long offset, long limit, boolean preferPinned) {
|
private @NonNull String createQuery(@NonNull String where, long offset, long limit, boolean preferPinned) {
|
||||||
String orderBy = (preferPinned ? TABLE_NAME + "." + PINNED + " DESC, " : "") + TABLE_NAME + "." + DATE + " DESC";
|
String orderBy = (preferPinned ? TABLE_NAME + "." + PINNED + " DESC, " : "") + TABLE_NAME + "." + DATE + " DESC";
|
||||||
|
|
||||||
return createQuery(where, orderBy, offset, limit, preferPinned);
|
return createQuery(where, orderBy, offset, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NonNull String createQuery(@NonNull String where, @NonNull String orderBy, long offset, long limit, boolean preferPinned) {
|
private @NonNull String createQuery(@NonNull String where, @NonNull String orderBy, long offset, long limit) {
|
||||||
String projection = Util.join(COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION, ",");
|
String projection = Util.join(COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION, ",");
|
||||||
|
|
||||||
String query =
|
String query =
|
||||||
|
|
|
@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
|
||||||
import org.signal.zkgroup.VerificationFailedException;
|
import org.signal.zkgroup.VerificationFailedException;
|
||||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
|
@ -81,10 +82,11 @@ public final class GroupManager {
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public static void migrateGroupToServer(@NonNull Context context,
|
public static void migrateGroupToServer(@NonNull Context context,
|
||||||
@NonNull GroupId.V1 groupIdV1)
|
@NonNull GroupId.V1 groupIdV1,
|
||||||
|
@NonNull Collection<Recipient> members)
|
||||||
throws IOException, GroupChangeFailedException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException
|
throws IOException, GroupChangeFailedException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException
|
||||||
{
|
{
|
||||||
new GroupManagerV2(context).migrateGroupOnToServer(groupIdV1);
|
new GroupManagerV2(context).migrateGroupOnToServer(groupIdV1, members);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Set<RecipientId> getMemberIds(Collection<Recipient> recipients) {
|
private static Set<RecipientId> getMemberIds(Collection<Recipient> recipients) {
|
||||||
|
@ -186,6 +188,19 @@ public final class GroupManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to gets the exact version of the group at the time you joined.
|
||||||
|
* <p>
|
||||||
|
* If it fails to get the exact version, it will give the latest.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
public static DecryptedGroup addedGroupVersion(@NonNull Context context,
|
||||||
|
@NonNull GroupMasterKey groupMasterKey)
|
||||||
|
throws IOException, GroupDoesNotExistException, GroupNotAMemberException
|
||||||
|
{
|
||||||
|
return new GroupManagerV2(context).addedGroupVersion(groupMasterKey);
|
||||||
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public static void setMemberAdmin(@NonNull Context context,
|
public static void setMemberAdmin(@NonNull Context context,
|
||||||
@NonNull GroupId.V2 groupId,
|
@NonNull GroupId.V2 groupId,
|
||||||
|
@ -371,6 +386,10 @@ public final class GroupManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void sendNoopUpdate(@NonNull Context context, @NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup currentState) {
|
||||||
|
new GroupManagerV2(context).sendNoopGroupUpdate(groupMasterKey, currentState);
|
||||||
|
}
|
||||||
|
|
||||||
public static class GroupActionResult {
|
public static class GroupActionResult {
|
||||||
private final Recipient groupRecipient;
|
private final Recipient groupRecipient;
|
||||||
private final long threadId;
|
private final long threadId;
|
||||||
|
|
|
@ -27,8 +27,8 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
|
||||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||||
|
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||||
|
@ -157,7 +157,7 @@ final class GroupManagerV1 {
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
static boolean leaveGroup(@NonNull Context context, @NonNull GroupId.V1 groupId) {
|
static boolean leaveGroup(@NonNull Context context, @NonNull GroupId.V1 groupId) {
|
||||||
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
|
Recipient groupRecipient = Recipient.externalGroupExact(context, groupId);
|
||||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
||||||
Optional<OutgoingGroupUpdateMessage> leaveMessage = createGroupLeaveMessage(context, groupId, groupRecipient);
|
Optional<OutgoingGroupUpdateMessage> leaveMessage = createGroupLeaveMessage(context, groupId, groupRecipient);
|
||||||
|
|
||||||
|
@ -183,7 +183,7 @@ final class GroupManagerV1 {
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
static boolean silentLeaveGroup(@NonNull Context context, @NonNull GroupId.V1 groupId) {
|
static boolean silentLeaveGroup(@NonNull Context context, @NonNull GroupId.V1 groupId) {
|
||||||
if (DatabaseFactory.getGroupDatabase(context).isActive(groupId)) {
|
if (DatabaseFactory.getGroupDatabase(context).isActive(groupId)) {
|
||||||
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
|
Recipient groupRecipient = Recipient.externalGroupExact(context, groupId);
|
||||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
||||||
Optional<OutgoingGroupUpdateMessage> leaveMessage = createGroupLeaveMessage(context, groupId, groupRecipient);
|
Optional<OutgoingGroupUpdateMessage> leaveMessage = createGroupLeaveMessage(context, groupId, groupRecipient);
|
||||||
|
|
||||||
|
@ -208,7 +208,7 @@ final class GroupManagerV1 {
|
||||||
static void updateGroupTimer(@NonNull Context context, @NonNull GroupId.V1 groupId, int expirationTime) {
|
static void updateGroupTimer(@NonNull Context context, @NonNull GroupId.V1 groupId, int expirationTime) {
|
||||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||||
Recipient recipient = Recipient.externalGroup(context, groupId);
|
Recipient recipient = Recipient.externalGroupExact(context, groupId);
|
||||||
long threadId = threadDatabase.getThreadIdFor(recipient);
|
long threadId = threadDatabase.getThreadIdFor(recipient);
|
||||||
|
|
||||||
recipientDatabase.setExpireMessages(recipient.getId(), expirationTime);
|
recipientDatabase.setExpireMessages(recipient.getId(), expirationTime);
|
||||||
|
@ -228,20 +228,6 @@ final class GroupManagerV1 {
|
||||||
return Optional.absent();
|
return Optional.absent();
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupContext groupContext = GroupContext.newBuilder()
|
return Optional.of(GroupUtil.createGroupV1LeaveMessage(groupId, groupRecipient));
|
||||||
.setId(ByteString.copyFrom(groupId.getDecodedId()))
|
|
||||||
.setType(GroupContext.Type.QUIT)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
return Optional.of(new OutgoingGroupUpdateMessage(groupRecipient,
|
|
||||||
groupContext,
|
|
||||||
null,
|
|
||||||
System.currentTimeMillis(),
|
|
||||||
0,
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
Collections.emptyList(),
|
|
||||||
Collections.emptyList(),
|
|
||||||
Collections.emptyList()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
@ -147,7 +148,34 @@ final class GroupManagerV2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
void migrateGroupOnToServer(@NonNull GroupId.V1 groupIdV1)
|
@NonNull DecryptedGroup addedGroupVersion(@NonNull GroupMasterKey groupMasterKey)
|
||||||
|
throws GroupNotAMemberException, IOException, GroupDoesNotExistException
|
||||||
|
{
|
||||||
|
GroupsV2StateProcessor.StateProcessorForGroup stateProcessorForGroup = new GroupsV2StateProcessor(context).forGroup(groupMasterKey);
|
||||||
|
DecryptedGroup latest = stateProcessorForGroup.getCurrentGroupStateFromServer();
|
||||||
|
|
||||||
|
if (latest.getRevision() == 0) {
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<DecryptedMember> selfInFullMemberList = DecryptedGroupUtil.findMemberByUuid(latest.getMembersList(), Recipient.self().requireUuid());
|
||||||
|
|
||||||
|
if (!selfInFullMemberList.isPresent()) {
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
|
DecryptedGroup joinedVersion = stateProcessorForGroup.getSpecificVersionFromServer(selfInFullMemberList.get().getJoinedAtRevision());
|
||||||
|
|
||||||
|
if (joinedVersion != null) {
|
||||||
|
return joinedVersion;
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Unable to retreive exact version joined at, using latest");
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
void migrateGroupOnToServer(@NonNull GroupId.V1 groupIdV1, @NonNull Collection<Recipient> members)
|
||||||
throws IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException, GroupChangeFailedException
|
throws IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException, GroupChangeFailedException
|
||||||
{
|
{
|
||||||
GroupMasterKey groupMasterKey = groupIdV1.deriveV2MigrationMasterKey();
|
GroupMasterKey groupMasterKey = groupIdV1.deriveV2MigrationMasterKey();
|
||||||
|
@ -156,13 +184,20 @@ final class GroupManagerV2 {
|
||||||
String name = groupRecord.getTitle();
|
String name = groupRecord.getTitle();
|
||||||
byte[] avatar = groupRecord.hasAvatar() ? AvatarHelper.getAvatarBytes(context, groupRecord.getRecipientId()) : null;
|
byte[] avatar = groupRecord.hasAvatar() ? AvatarHelper.getAvatarBytes(context, groupRecord.getRecipientId()) : null;
|
||||||
int messageTimer = Recipient.resolved(groupRecord.getRecipientId()).getExpireMessages();
|
int messageTimer = Recipient.resolved(groupRecord.getRecipientId()).getExpireMessages();
|
||||||
Set<RecipientId> memberIds = Stream.of(groupRecord.getMembers())
|
Set<RecipientId> memberIds = Stream.of(members)
|
||||||
|
.map(Recipient::getId)
|
||||||
.filterNot(m -> m.equals(Recipient.self().getId()))
|
.filterNot(m -> m.equals(Recipient.self().getId()))
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
createGroupOnServer(groupSecretParams, name, avatar, memberIds, Member.Role.ADMINISTRATOR, messageTimer);
|
createGroupOnServer(groupSecretParams, name, avatar, memberIds, Member.Role.ADMINISTRATOR, messageTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
void sendNoopGroupUpdate(@NonNull GroupMasterKey masterKey, @NonNull DecryptedGroup currentState) {
|
||||||
|
sendGroupUpdate(masterKey, new GroupMutation(currentState, DecryptedGroupChange.newBuilder().build(), currentState), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
final class GroupCreator extends LockOwner {
|
final class GroupCreator extends LockOwner {
|
||||||
|
|
||||||
GroupCreator(@NonNull Closeable lock) {
|
GroupCreator(@NonNull Closeable lock) {
|
||||||
|
@ -290,7 +325,7 @@ final class GroupManagerV2 {
|
||||||
GroupManager.GroupActionResult groupActionResult = commitChangeWithConflictResolution(change);
|
GroupManager.GroupActionResult groupActionResult = commitChangeWithConflictResolution(change);
|
||||||
|
|
||||||
if (avatarChanged) {
|
if (avatarChanged) {
|
||||||
AvatarHelper.setAvatar(context, Recipient.externalGroup(context, groupId).getId(), avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null);
|
AvatarHelper.setAvatar(context, Recipient.externalGroupExact(context, groupId).getId(), avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null);
|
||||||
groupDatabase.onAvatarUpdated(groupId, avatarBytes != null);
|
groupDatabase.onAvatarUpdated(groupId, avatarBytes != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -479,7 +514,7 @@ final class GroupManagerV2 {
|
||||||
|
|
||||||
if (GroupChangeUtil.changeIsEmpty(change.build())) {
|
if (GroupChangeUtil.changeIsEmpty(change.build())) {
|
||||||
Log.i(TAG, "Change is empty after conflict resolution");
|
Log.i(TAG, "Change is empty after conflict resolution");
|
||||||
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
|
Recipient groupRecipient = Recipient.externalGroupExact(context, groupId);
|
||||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
||||||
|
|
||||||
return new GroupManager.GroupActionResult(groupRecipient, threadId, 0, Collections.emptyList());
|
return new GroupManager.GroupActionResult(groupRecipient, threadId, 0, Collections.emptyList());
|
||||||
|
@ -1026,7 +1061,7 @@ final class GroupManagerV2 {
|
||||||
@Nullable GroupChange signedGroupChange)
|
@Nullable GroupChange signedGroupChange)
|
||||||
{
|
{
|
||||||
GroupId.V2 groupId = GroupId.v2(masterKey);
|
GroupId.V2 groupId = GroupId.v2(masterKey);
|
||||||
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
|
Recipient groupRecipient = Recipient.externalGroupExact(context, groupId);
|
||||||
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, groupMutation, signedGroupChange);
|
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, groupMutation, signedGroupChange);
|
||||||
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient,
|
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient,
|
||||||
decryptedGroupV2Context,
|
decryptedGroupV2Context,
|
||||||
|
|
|
@ -9,13 +9,10 @@ import androidx.annotation.Nullable;
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.Database;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
|
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.jobs.AvatarGroupsV1DownloadJob;
|
import org.thoughtcrime.securesms.jobs.AvatarGroupsV1DownloadJob;
|
||||||
import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob;
|
import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob;
|
||||||
|
@ -24,11 +21,9 @@ import org.thoughtcrime.securesms.mms.MmsException;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
|
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
|
||||||
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage;
|
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage;
|
||||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||||
import org.thoughtcrime.securesms.util.Base64;
|
import org.thoughtcrime.securesms.util.Base64;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
||||||
|
@ -114,7 +109,7 @@ public final class GroupV1MessageProcessor {
|
||||||
|
|
||||||
if (sender.isSystemContact() || sender.isProfileSharing()) {
|
if (sender.isSystemContact() || sender.isProfileSharing()) {
|
||||||
Log.i(TAG, "Auto-enabling profile sharing because 'adder' is trusted. contact: " + sender.isSystemContact() + ", profileSharing: " + sender.isProfileSharing());
|
Log.i(TAG, "Auto-enabling profile sharing because 'adder' is trusted. contact: " + sender.isSystemContact() + ", profileSharing: " + sender.isProfileSharing());
|
||||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.externalGroup(context, id).getId(), true);
|
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.externalGroupExact(context, id).getId(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return storeMessage(context, content, group, builder.build(), outgoing);
|
return storeMessage(context, content, group, builder.build(), outgoing);
|
||||||
|
|
|
@ -82,7 +82,7 @@ public final class LiveGroup {
|
||||||
this.groupLink = new MutableLiveData<>(GroupLinkUrlAndStatus.NONE);
|
this.groupLink = new MutableLiveData<>(GroupLinkUrlAndStatus.NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
SignalExecutors.BOUNDED.execute(() -> liveRecipient.postValue(Recipient.externalGroup(context, groupId).live()));
|
SignalExecutors.BOUNDED.execute(() -> liveRecipient.postValue(Recipient.externalGroupExact(context, groupId).live()));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static LiveData<List<GroupMemberEntry.FullMember>> mapToFullMembers(@NonNull LiveData<GroupDatabase.GroupRecord> groupRecord) {
|
protected static LiveData<List<GroupMemberEntry.FullMember>> mapToFullMembers(@NonNull LiveData<GroupDatabase.GroupRecord> groupRecord) {
|
||||||
|
|
|
@ -110,7 +110,7 @@ public final class ChooseNewAdminActivity extends PassphraseRequiredActivity {
|
||||||
|
|
||||||
private void handleUpdateAndLeaveResult(@NonNull GroupChangeResult updateResult) {
|
private void handleUpdateAndLeaveResult(@NonNull GroupChangeResult updateResult) {
|
||||||
if (updateResult.isSuccess()) {
|
if (updateResult.isSuccess()) {
|
||||||
String title = Recipient.externalGroup(this, groupId).getDisplayName(this);
|
String title = Recipient.externalGroupExact(this, groupId).getDisplayName(this);
|
||||||
Toast.makeText(this, getString(R.string.ChooseNewAdminActivity_you_left, title), Toast.LENGTH_LONG).show();
|
Toast.makeText(this, getString(R.string.ChooseNewAdminActivity_you_left, title), Toast.LENGTH_LONG).show();
|
||||||
startActivity(new Intent(this, MainActivity.class));
|
startActivity(new Intent(this, MainActivity.class));
|
||||||
finish();
|
finish();
|
||||||
|
|
|
@ -42,22 +42,16 @@ final class ManageGroupRepository {
|
||||||
private static final String TAG = Log.tag(ManageGroupRepository.class);
|
private static final String TAG = Log.tag(ManageGroupRepository.class);
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final GroupId groupId;
|
|
||||||
|
|
||||||
ManageGroupRepository(@NonNull Context context, @NonNull GroupId groupId) {
|
ManageGroupRepository(@NonNull Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.groupId = groupId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupId getGroupId() {
|
void getGroupState(@NonNull GroupId groupId, @NonNull Consumer<GroupStateResult> onGroupStateLoaded) {
|
||||||
return groupId;
|
SignalExecutors.BOUNDED.execute(() -> onGroupStateLoaded.accept(getGroupState(groupId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void getGroupState(@NonNull Consumer<GroupStateResult> onGroupStateLoaded) {
|
void getGroupCapacity(@NonNull GroupId groupId, @NonNull Consumer<GroupCapacityResult> onGroupCapacityLoaded) {
|
||||||
SignalExecutors.BOUNDED.execute(() -> onGroupStateLoaded.accept(getGroupState()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void getGroupCapacity(@NonNull Consumer<GroupCapacityResult> onGroupCapacityLoaded) {
|
|
||||||
SimpleTask.run(SignalExecutors.BOUNDED, () -> {
|
SimpleTask.run(SignalExecutors.BOUNDED, () -> {
|
||||||
GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get();
|
GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get();
|
||||||
if (groupRecord.isV2Group()) {
|
if (groupRecord.isV2Group()) {
|
||||||
|
@ -77,15 +71,15 @@ final class ManageGroupRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private GroupStateResult getGroupState() {
|
private GroupStateResult getGroupState(@NonNull GroupId groupId) {
|
||||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||||
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
|
Recipient groupRecipient = Recipient.externalGroupExact(context, groupId);
|
||||||
long threadId = threadDatabase.getThreadIdFor(groupRecipient);
|
long threadId = threadDatabase.getThreadIdFor(groupRecipient);
|
||||||
|
|
||||||
return new GroupStateResult(threadId, groupRecipient);
|
return new GroupStateResult(threadId, groupRecipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setExpiration(int newExpirationTime, @NonNull GroupChangeErrorCallback error) {
|
void setExpiration(@NonNull GroupId groupId, int newExpirationTime, @NonNull GroupChangeErrorCallback error) {
|
||||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||||
try {
|
try {
|
||||||
GroupManager.updateGroupTimer(context, groupId.requirePush(), newExpirationTime);
|
GroupManager.updateGroupTimer(context, groupId.requirePush(), newExpirationTime);
|
||||||
|
@ -96,7 +90,7 @@ final class ManageGroupRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void applyMembershipRightsChange(@NonNull GroupAccessControl newRights, @NonNull GroupChangeErrorCallback error) {
|
void applyMembershipRightsChange(@NonNull GroupId groupId, @NonNull GroupAccessControl newRights, @NonNull GroupChangeErrorCallback error) {
|
||||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||||
try {
|
try {
|
||||||
GroupManager.applyMembershipAdditionRightsChange(context, groupId.requireV2(), newRights);
|
GroupManager.applyMembershipAdditionRightsChange(context, groupId.requireV2(), newRights);
|
||||||
|
@ -107,7 +101,7 @@ final class ManageGroupRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void applyAttributesRightsChange(@NonNull GroupAccessControl newRights, @NonNull GroupChangeErrorCallback error) {
|
void applyAttributesRightsChange(@NonNull GroupId groupId, @NonNull GroupAccessControl newRights, @NonNull GroupChangeErrorCallback error) {
|
||||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||||
try {
|
try {
|
||||||
GroupManager.applyAttributesRightsChange(context, groupId.requireV2(), newRights);
|
GroupManager.applyAttributesRightsChange(context, groupId.requireV2(), newRights);
|
||||||
|
@ -118,20 +112,21 @@ final class ManageGroupRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void getRecipient(@NonNull Consumer<Recipient> recipientCallback) {
|
public void getRecipient(@NonNull GroupId groupId, @NonNull Consumer<Recipient> recipientCallback) {
|
||||||
SimpleTask.run(SignalExecutors.BOUNDED,
|
SimpleTask.run(SignalExecutors.BOUNDED,
|
||||||
() -> Recipient.externalGroup(context, groupId),
|
() -> Recipient.externalGroupExact(context, groupId),
|
||||||
recipientCallback::accept);
|
recipientCallback::accept);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMuteUntil(long until) {
|
void setMuteUntil(@NonNull GroupId groupId, long until) {
|
||||||
SignalExecutors.BOUNDED.execute(() -> {
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
RecipientId recipientId = Recipient.externalGroup(context, groupId).getId();
|
RecipientId recipientId = Recipient.externalGroupExact(context, groupId).getId();
|
||||||
DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until);
|
DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void addMembers(@NonNull List<RecipientId> selected,
|
void addMembers(@NonNull GroupId groupId,
|
||||||
|
@NonNull List<RecipientId> selected,
|
||||||
@NonNull AsynchronousCallback.WorkerThread<ManageGroupViewModel.AddMembersResult, GroupChangeFailureReason> callback)
|
@NonNull AsynchronousCallback.WorkerThread<ManageGroupViewModel.AddMembersResult, GroupChangeFailureReason> callback)
|
||||||
{
|
{
|
||||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||||
|
@ -145,10 +140,10 @@ final class ManageGroupRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void blockAndLeaveGroup(@NonNull GroupChangeErrorCallback error, @NonNull Runnable onSuccess) {
|
void blockAndLeaveGroup(@NonNull GroupId groupId, @NonNull GroupChangeErrorCallback error, @NonNull Runnable onSuccess) {
|
||||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||||
try {
|
try {
|
||||||
RecipientUtil.block(context, Recipient.externalGroup(context, groupId));
|
RecipientUtil.block(context, Recipient.externalGroupExact(context, groupId));
|
||||||
onSuccess.run();
|
onSuccess.run();
|
||||||
} catch (GroupChangeException | IOException e) {
|
} catch (GroupChangeException | IOException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
|
@ -157,9 +152,9 @@ final class ManageGroupRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMentionSetting(RecipientDatabase.MentionSetting mentionSetting) {
|
void setMentionSetting(@NonNull GroupId groupId, RecipientDatabase.MentionSetting mentionSetting) {
|
||||||
SignalExecutors.BOUNDED.execute(() -> {
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
RecipientId recipientId = Recipient.externalGroup(context, groupId).getId();
|
RecipientId recipientId = Recipient.externalGroupExact(context, groupId).getId();
|
||||||
DatabaseFactory.getRecipientDatabase(context).setMentionSetting(recipientId, mentionSetting);
|
DatabaseFactory.getRecipientDatabase(context).setMentionSetting(recipientId, mentionSetting);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,18 +81,18 @@ public class ManageGroupViewModel extends ViewModel {
|
||||||
private final LiveData<Boolean> groupLinkOn;
|
private final LiveData<Boolean> groupLinkOn;
|
||||||
private final LiveData<GroupInfoMessage> groupInfoMessage;
|
private final LiveData<GroupInfoMessage> groupInfoMessage;
|
||||||
|
|
||||||
private ManageGroupViewModel(@NonNull Context context, @NonNull ManageGroupRepository manageGroupRepository) {
|
private ManageGroupViewModel(@NonNull Context context, @NonNull GroupId groupId, @NonNull ManageGroupRepository manageGroupRepository) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.manageGroupRepository = manageGroupRepository;
|
this.manageGroupRepository = manageGroupRepository;
|
||||||
|
|
||||||
manageGroupRepository.getGroupState(this::groupStateLoaded);
|
manageGroupRepository.getGroupState(groupId, this::groupStateLoaded);
|
||||||
|
|
||||||
GroupId groupId = manageGroupRepository.getGroupId();
|
|
||||||
LiveGroup liveGroup = new LiveGroup(groupId);
|
LiveGroup liveGroup = new LiveGroup(groupId);
|
||||||
|
|
||||||
this.title = Transformations.map(liveGroup.getTitle(),
|
this.title = Transformations.map(liveGroup.getTitle(),
|
||||||
title -> TextUtils.isEmpty(title) ? context.getString(R.string.Recipient_unknown)
|
title -> TextUtils.isEmpty(title) ? context.getString(R.string.Recipient_unknown)
|
||||||
: title);
|
: title);
|
||||||
|
this.groupRecipient = liveGroup.getGroupRecipient();
|
||||||
this.isAdmin = liveGroup.isSelfAdmin();
|
this.isAdmin = liveGroup.isSelfAdmin();
|
||||||
this.canCollapseMemberList = LiveDataUtil.combineLatest(memberListCollapseState,
|
this.canCollapseMemberList = LiveDataUtil.combineLatest(memberListCollapseState,
|
||||||
Transformations.map(liveGroup.getFullMembers(), m -> m.size() > MAX_UNCOLLAPSED_MEMBERS),
|
Transformations.map(liveGroup.getFullMembers(), m -> m.size() > MAX_UNCOLLAPSED_MEMBERS),
|
||||||
|
@ -102,7 +102,7 @@ public class ManageGroupViewModel extends ViewModel {
|
||||||
ManageGroupViewModel::filterMemberList);
|
ManageGroupViewModel::filterMemberList);
|
||||||
this.pendingMemberCount = liveGroup.getPendingMemberCount();
|
this.pendingMemberCount = liveGroup.getPendingMemberCount();
|
||||||
this.pendingAndRequestingCount = liveGroup.getPendingAndRequestingMemberCount();
|
this.pendingAndRequestingCount = liveGroup.getPendingAndRequestingMemberCount();
|
||||||
this.showLegacyIndicator = new MutableLiveData<>(groupId.isV1());
|
this.showLegacyIndicator = Transformations.map(groupRecipient, recipient -> recipient.requireGroupId().isV1());
|
||||||
this.memberCountSummary = LiveDataUtil.combineLatest(liveGroup.getMembershipCountDescription(context.getResources()),
|
this.memberCountSummary = LiveDataUtil.combineLatest(liveGroup.getMembershipCountDescription(context.getResources()),
|
||||||
this.showLegacyIndicator,
|
this.showLegacyIndicator,
|
||||||
(description, legacy) -> legacy ? String.format("%s · %s", description, context.getString(R.string.ManageGroupActivity_legacy_group))
|
(description, legacy) -> legacy ? String.format("%s · %s", description, context.getString(R.string.ManageGroupActivity_legacy_group))
|
||||||
|
@ -113,7 +113,6 @@ public class ManageGroupViewModel extends ViewModel {
|
||||||
this.disappearingMessageTimer = Transformations.map(liveGroup.getExpireMessages(), expiration -> ExpirationUtil.getExpirationDisplayValue(context, expiration));
|
this.disappearingMessageTimer = Transformations.map(liveGroup.getExpireMessages(), expiration -> ExpirationUtil.getExpirationDisplayValue(context, expiration));
|
||||||
this.canEditGroupAttributes = liveGroup.selfCanEditGroupAttributes();
|
this.canEditGroupAttributes = liveGroup.selfCanEditGroupAttributes();
|
||||||
this.canAddMembers = liveGroup.selfCanAddMembers();
|
this.canAddMembers = liveGroup.selfCanAddMembers();
|
||||||
this.groupRecipient = liveGroup.getGroupRecipient();
|
|
||||||
this.muteState = Transformations.map(this.groupRecipient,
|
this.muteState = Transformations.map(this.groupRecipient,
|
||||||
recipient -> new MuteState(recipient.getMuteUntil(), recipient.isMuted()));
|
recipient -> new MuteState(recipient.getMuteUntil(), recipient.isMuted()));
|
||||||
this.hasCustomNotifications = Transformations.map(this.groupRecipient,
|
this.hasCustomNotifications = Transformations.map(this.groupRecipient,
|
||||||
|
@ -231,44 +230,47 @@ public class ManageGroupViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleExpirationSelection() {
|
void handleExpirationSelection() {
|
||||||
manageGroupRepository.getRecipient(groupRecipient ->
|
manageGroupRepository.getRecipient(getGroupId(),
|
||||||
|
groupRecipient ->
|
||||||
ExpirationDialog.show(context,
|
ExpirationDialog.show(context,
|
||||||
groupRecipient.getExpireMessages(),
|
groupRecipient.getExpireMessages(),
|
||||||
expirationTime -> manageGroupRepository.setExpiration(expirationTime, this::showErrorToast)));
|
expirationTime -> manageGroupRepository.setExpiration(getGroupId(), expirationTime, this::showErrorToast)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void applyMembershipRightsChange(@NonNull GroupAccessControl newRights) {
|
void applyMembershipRightsChange(@NonNull GroupAccessControl newRights) {
|
||||||
manageGroupRepository.applyMembershipRightsChange(newRights, this::showErrorToast);
|
manageGroupRepository.applyMembershipRightsChange(getGroupId(), newRights, this::showErrorToast);
|
||||||
}
|
}
|
||||||
|
|
||||||
void applyAttributesRightsChange(@NonNull GroupAccessControl newRights) {
|
void applyAttributesRightsChange(@NonNull GroupAccessControl newRights) {
|
||||||
manageGroupRepository.applyAttributesRightsChange(newRights, this::showErrorToast);
|
manageGroupRepository.applyAttributesRightsChange(getGroupId(), newRights, this::showErrorToast);
|
||||||
}
|
}
|
||||||
|
|
||||||
void blockAndLeave(@NonNull FragmentActivity activity) {
|
void blockAndLeave(@NonNull FragmentActivity activity) {
|
||||||
manageGroupRepository.getRecipient(recipient -> BlockUnblockDialog.showBlockFor(activity,
|
manageGroupRepository.getRecipient(getGroupId(),
|
||||||
|
recipient -> BlockUnblockDialog.showBlockFor(activity,
|
||||||
activity.getLifecycle(),
|
activity.getLifecycle(),
|
||||||
recipient,
|
recipient,
|
||||||
this::onBlockAndLeaveConfirmed));
|
this::onBlockAndLeaveConfirmed));
|
||||||
}
|
}
|
||||||
|
|
||||||
void unblock(@NonNull FragmentActivity activity) {
|
void unblock(@NonNull FragmentActivity activity) {
|
||||||
manageGroupRepository.getRecipient(recipient -> BlockUnblockDialog.showUnblockFor(activity, activity.getLifecycle(), recipient,
|
manageGroupRepository.getRecipient(getGroupId(),
|
||||||
|
recipient -> BlockUnblockDialog.showUnblockFor(activity, activity.getLifecycle(), recipient,
|
||||||
() -> RecipientUtil.unblock(context, recipient)));
|
() -> RecipientUtil.unblock(context, recipient)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onAddMembers(@NonNull List<RecipientId> selected,
|
void onAddMembers(@NonNull List<RecipientId> selected,
|
||||||
@NonNull AsynchronousCallback.MainThread<AddMembersResult, GroupChangeFailureReason> callback)
|
@NonNull AsynchronousCallback.MainThread<AddMembersResult, GroupChangeFailureReason> callback)
|
||||||
{
|
{
|
||||||
manageGroupRepository.addMembers(selected, callback.toWorkerCallback());
|
manageGroupRepository.addMembers(getGroupId(), selected, callback.toWorkerCallback());
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMuteUntil(long muteUntil) {
|
void setMuteUntil(long muteUntil) {
|
||||||
manageGroupRepository.setMuteUntil(muteUntil);
|
manageGroupRepository.setMuteUntil(getGroupId(), muteUntil);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearMuteUntil() {
|
void clearMuteUntil() {
|
||||||
manageGroupRepository.setMuteUntil(0);
|
manageGroupRepository.setMuteUntil(getGroupId(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void revealCollapsedMembers() {
|
void revealCollapsedMembers() {
|
||||||
|
@ -276,19 +278,24 @@ public class ManageGroupViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleMentionNotificationSelection() {
|
void handleMentionNotificationSelection() {
|
||||||
manageGroupRepository.getRecipient(r -> GroupMentionSettingDialog.show(context, r.getMentionSetting(), manageGroupRepository::setMentionSetting));
|
manageGroupRepository.getRecipient(getGroupId(), r -> GroupMentionSettingDialog.show(context, r.getMentionSetting(), setting -> manageGroupRepository.setMentionSetting(getGroupId(), setting)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onBlockAndLeaveConfirmed() {
|
private void onBlockAndLeaveConfirmed() {
|
||||||
SimpleProgressDialog.DismissibleDialog dismissibleDialog = SimpleProgressDialog.showDelayed(context);
|
SimpleProgressDialog.DismissibleDialog dismissibleDialog = SimpleProgressDialog.showDelayed(context);
|
||||||
|
|
||||||
manageGroupRepository.blockAndLeaveGroup(e -> {
|
manageGroupRepository.blockAndLeaveGroup(getGroupId(),
|
||||||
|
e -> {
|
||||||
dismissibleDialog.dismiss();
|
dismissibleDialog.dismiss();
|
||||||
showErrorToast(e);
|
showErrorToast(e);
|
||||||
},
|
},
|
||||||
dismissibleDialog::dismiss);
|
dismissibleDialog::dismiss);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private @NonNull GroupId getGroupId() {
|
||||||
|
return groupRecipient.getValue().requireGroupId();
|
||||||
|
}
|
||||||
|
|
||||||
private static @NonNull List<GroupMemberEntry.FullMember> filterMemberList(@NonNull List<GroupMemberEntry.FullMember> members,
|
private static @NonNull List<GroupMemberEntry.FullMember> filterMemberList(@NonNull List<GroupMemberEntry.FullMember> members,
|
||||||
@NonNull CollapseState collapseState)
|
@NonNull CollapseState collapseState)
|
||||||
{
|
{
|
||||||
|
@ -305,13 +312,13 @@ public class ManageGroupViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onAddMembersClick(@NonNull Fragment fragment, int resultCode) {
|
public void onAddMembersClick(@NonNull Fragment fragment, int resultCode) {
|
||||||
manageGroupRepository.getGroupCapacity(capacity -> {
|
manageGroupRepository.getGroupCapacity(getGroupId(), capacity -> {
|
||||||
int remainingCapacity = capacity.getRemainingCapacity();
|
int remainingCapacity = capacity.getRemainingCapacity();
|
||||||
if (remainingCapacity <= 0) {
|
if (remainingCapacity <= 0) {
|
||||||
GroupLimitDialog.showHardLimitMessage(fragment.requireContext());
|
GroupLimitDialog.showHardLimitMessage(fragment.requireContext());
|
||||||
} else {
|
} else {
|
||||||
Intent intent = new Intent(fragment.requireActivity(), AddMembersActivity.class);
|
Intent intent = new Intent(fragment.requireActivity(), AddMembersActivity.class);
|
||||||
intent.putExtra(AddMembersActivity.GROUP_ID, manageGroupRepository.getGroupId().toString());
|
intent.putExtra(AddMembersActivity.GROUP_ID, getGroupId().toString());
|
||||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_PUSH);
|
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_PUSH);
|
||||||
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, new SelectionLimits(capacity.getSelectionWarning(), capacity.getSelectionLimit()));
|
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, new SelectionLimits(capacity.getSelectionWarning(), capacity.getSelectionLimit()));
|
||||||
intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, capacity.getMembersWithoutSelf());
|
intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, capacity.getMembersWithoutSelf());
|
||||||
|
@ -410,7 +417,7 @@ public class ManageGroupViewModel extends ViewModel {
|
||||||
@Override
|
@Override
|
||||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
return (T) new ManageGroupViewModel(context, new ManageGroupRepository(context.getApplicationContext(), groupId));
|
return (T) new ManageGroupViewModel(context, groupId, new ManageGroupRepository(context.getApplicationContext()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,7 +245,7 @@ public final class GroupsV2StateProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public DecryptedGroup getCurrentGroupStateFromServer()
|
public @NonNull DecryptedGroup getCurrentGroupStateFromServer()
|
||||||
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
|
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
@ -259,13 +259,31 @@ public final class GroupsV2StateProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
public @Nullable DecryptedGroup getSpecificVersionFromServer(int revision)
|
||||||
|
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return groupsV2Api.getGroupHistory(groupSecretParams, revision, groupsV2Authorization.getAuthorizationForToday(Recipient.self().requireUuid(), groupSecretParams))
|
||||||
|
.get(0)
|
||||||
|
.getGroup()
|
||||||
|
.orNull();
|
||||||
|
} catch (GroupNotFoundException e) {
|
||||||
|
throw new GroupDoesNotExistException(e);
|
||||||
|
} catch (NotInGroupException e) {
|
||||||
|
throw new GroupNotAMemberException(e);
|
||||||
|
} catch (VerificationFailedException | InvalidGroupStateException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void insertGroupLeave() {
|
private void insertGroupLeave() {
|
||||||
if (!groupDatabase.isActive(groupId)) {
|
if (!groupDatabase.isActive(groupId)) {
|
||||||
Log.w(TAG, "Group has already been left.");
|
Log.w(TAG, "Group has already been left.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
|
Recipient groupRecipient = Recipient.externalGroupExact(context, groupId);
|
||||||
UUID selfUuid = Recipient.self().getUuid().get();
|
UUID selfUuid = Recipient.self().getUuid().get();
|
||||||
DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId)
|
DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId)
|
||||||
.requireV2GroupProperties()
|
.requireV2GroupProperties()
|
||||||
|
@ -368,7 +386,7 @@ public final class GroupsV2StateProcessor {
|
||||||
if (addedBy.isSystemContact() || addedBy.isProfileSharing()) {
|
if (addedBy.isSystemContact() || addedBy.isProfileSharing()) {
|
||||||
Log.i(TAG, "Group 'adder' is trusted. contact: " + addedBy.isSystemContact() + ", profileSharing: " + addedBy.isProfileSharing());
|
Log.i(TAG, "Group 'adder' is trusted. contact: " + addedBy.isSystemContact() + ", profileSharing: " + addedBy.isProfileSharing());
|
||||||
Log.i(TAG, "Added to a group and auto-enabling profile sharing");
|
Log.i(TAG, "Added to a group and auto-enabling profile sharing");
|
||||||
recipientDatabase.setProfileSharing(Recipient.externalGroup(context, groupId).getId(), true);
|
recipientDatabase.setProfileSharing(Recipient.externalGroupExact(context, groupId).getId(), true);
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "Added to a group, but not enabling profile sharing, as 'adder' is not trusted");
|
Log.i(TAG, "Added to a group, but not enabling profile sharing, as 'adder' is not trusted");
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ public class PushProcessMessageQueueJobMigration extends JobMigration {
|
||||||
Log.i(TAG, "Migrating a group message.");
|
Log.i(TAG, "Migrating a group message.");
|
||||||
try {
|
try {
|
||||||
GroupId groupId = GroupUtil.idFromGroupContext(content.getDataMessage().get().getGroupContext().get());
|
GroupId groupId = GroupUtil.idFromGroupContext(content.getDataMessage().get().getGroupContext().get());
|
||||||
Recipient recipient = Recipient.externalGroup(context, groupId);
|
Recipient recipient = Recipient.externalGroupExact(context, groupId);
|
||||||
|
|
||||||
suffix = recipient.getId().toQueueKey();
|
suffix = recipient.getId().toQueueKey();
|
||||||
} catch (BadGroupIdException e) {
|
} catch (BadGroupIdException e) {
|
||||||
|
@ -75,7 +75,7 @@ public class PushProcessMessageQueueJobMigration extends JobMigration {
|
||||||
GroupId exceptionGroup = GroupId.parseNullableOrThrow(data.getStringOrDefault("exception_groupId", null));
|
GroupId exceptionGroup = GroupId.parseNullableOrThrow(data.getStringOrDefault("exception_groupId", null));
|
||||||
|
|
||||||
if (exceptionGroup != null) {
|
if (exceptionGroup != null) {
|
||||||
suffix = Recipient.externalGroup(context, exceptionGroup).getId().toQueueKey();
|
suffix = Recipient.externalGroupExact(context, exceptionGroup).getId().toQueueKey();
|
||||||
} else if (exceptionSender != null) {
|
} else if (exceptionSender != null) {
|
||||||
suffix = Recipient.external(context, exceptionSender).getId().toQueueKey();
|
suffix = Recipient.external(context, exceptionSender).getId().toQueueKey();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,352 @@
|
||||||
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupAlreadyExistsException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupDoesNotExistException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
||||||
|
import org.thoughtcrime.securesms.groups.MembershipNotSuitableForV2Exception;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.mms.MmsException;
|
||||||
|
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
|
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
|
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor.LATEST;
|
||||||
|
|
||||||
|
public class GroupV1MigrationJob extends BaseJob {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(GroupV1MigrationJob.class);
|
||||||
|
|
||||||
|
public static final String KEY = "GroupV1MigrationJob";
|
||||||
|
|
||||||
|
private static final String KEY_RECIPIENT_ID = "recipient_id";
|
||||||
|
private static final String KEY_FORCED = "forced";
|
||||||
|
|
||||||
|
private static final int ROUTINE_LIMIT = 50;
|
||||||
|
private static final long REFRESH_INTERVAL = TimeUnit.HOURS.toMillis(3);
|
||||||
|
|
||||||
|
private final RecipientId recipientId;
|
||||||
|
private final boolean forced;
|
||||||
|
|
||||||
|
private GroupV1MigrationJob(@NonNull RecipientId recipientId, boolean forced) {
|
||||||
|
this(updateParameters(new Parameters.Builder()
|
||||||
|
.setQueue(recipientId.toQueueKey())
|
||||||
|
.addConstraint(NetworkConstraint.KEY),
|
||||||
|
forced),
|
||||||
|
recipientId,
|
||||||
|
forced);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Parameters updateParameters(@NonNull Parameters.Builder builder, boolean forced) {
|
||||||
|
if (forced) {
|
||||||
|
return builder.setMaxAttempts(Parameters.UNLIMITED)
|
||||||
|
.setLifespan(TimeUnit.DAYS.toMillis(7))
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
return builder.setMaxAttempts(3)
|
||||||
|
.setLifespan(TimeUnit.MINUTES.toMillis(20))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupV1MigrationJob(@NonNull Parameters parameters, @NonNull RecipientId recipientId, boolean forced) {
|
||||||
|
super(parameters);
|
||||||
|
this.recipientId = recipientId;
|
||||||
|
this.forced = forced;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void enqueuePossibleAutoMigrate(@NonNull RecipientId recipientId) {
|
||||||
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
|
if (Recipient.resolved(recipientId).isPushV1Group()) {
|
||||||
|
ApplicationDependencies.getJobManager().add(new GroupV1MigrationJob(recipientId, false));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void enqueueRoutineMigrationsIfNecessary(@NonNull Application application) {
|
||||||
|
if (!SignalStore.registrationValues().isRegistrationComplete() ||
|
||||||
|
!TextSecurePreferences.isPushRegistered(application) ||
|
||||||
|
TextSecurePreferences.getLocalUuid(application) == null)
|
||||||
|
{
|
||||||
|
Log.i(TAG, "Registration not complete. Skipping.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FeatureFlags.groupsV1AutoMigration()) {
|
||||||
|
Log.i(TAG, "Auto-migration disabled. Not proactively searching for groups.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long timeSinceRefresh = System.currentTimeMillis() - SignalStore.misc().getLastProfileRefreshTime();
|
||||||
|
if (timeSinceRefresh < REFRESH_INTERVAL) {
|
||||||
|
Log.i(TAG, "Too soon to refresh. Did the last refresh " + timeSinceRefresh + " ms ago.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
|
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||||
|
List<ThreadRecord> threads = DatabaseFactory.getThreadDatabase(application).getRecentV1Groups(ROUTINE_LIMIT);
|
||||||
|
Set<RecipientId> needsRefresh = new HashSet<>();
|
||||||
|
|
||||||
|
if (threads.size() > 0) {
|
||||||
|
Log.d(TAG, "About to enqueue refreshes for " + threads.size() + " groups.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ThreadRecord thread : threads) {
|
||||||
|
jobManager.add(new GroupV1MigrationJob(thread.getRecipient().getId(), false));
|
||||||
|
|
||||||
|
needsRefresh.addAll(Stream.of(thread.getRecipient().getParticipants())
|
||||||
|
.filter(r -> r.getGroupsV2Capability() != Recipient.Capability.SUPPORTED ||
|
||||||
|
r.getGroupsV1MigrationCapability() != Recipient.Capability.SUPPORTED)
|
||||||
|
.map(Recipient::getId)
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsRefresh.size() > 0) {
|
||||||
|
Log.w(TAG, "Enqueuing profile refreshes for " + needsRefresh.size() + " GV1 participants.");
|
||||||
|
RetrieveProfileJob.enqueue(needsRefresh);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull Data serialize() {
|
||||||
|
return new Data.Builder().putString(KEY_RECIPIENT_ID, recipientId.serialize())
|
||||||
|
.putBoolean(KEY_FORCED, forced)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull String getFactoryKey() {
|
||||||
|
return KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRun() throws IOException, RetryLaterException {
|
||||||
|
Recipient groupRecipient = Recipient.resolved(recipientId);
|
||||||
|
Long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipientId);
|
||||||
|
|
||||||
|
if (threadId == null) {
|
||||||
|
warn(TAG, "No thread found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groupRecipient.isPushV1Group()) {
|
||||||
|
warn(TAG, "Not a V1 group!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupRecipient.getParticipants().size() > FeatureFlags.groupLimits().getHardLimit()) {
|
||||||
|
warn(TAG, "Too many members! Size: " + groupRecipient.getParticipants().size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupId.V1 gv1Id = groupRecipient.requireGroupId().requireV1();
|
||||||
|
GroupId.V2 gv2Id = gv1Id.deriveV2MigrationGroupId();
|
||||||
|
GroupMasterKey gv2MasterKey = gv1Id.deriveV2MigrationMasterKey();
|
||||||
|
boolean newlyCreated = false;
|
||||||
|
|
||||||
|
switch (GroupManager.v2GroupStatus(context, gv2MasterKey)) {
|
||||||
|
case DOES_NOT_EXIST:
|
||||||
|
log(TAG, "Group does not exist on the service.");
|
||||||
|
|
||||||
|
if (!groupRecipient.isActiveGroup()) {
|
||||||
|
warn(TAG, "Group is inactive! Can't migrate.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groupRecipient.isProfileSharing()) {
|
||||||
|
warn(TAG, "Profile sharing is disabled! Can't migrate.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!forced && SignalStore.internalValues().disableGv1AutoMigrateInitiation()) {
|
||||||
|
warn(TAG, "Auto migration initiation has been disabled! Skipping.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!forced && !FeatureFlags.groupsV1AutoMigration()) {
|
||||||
|
warn(TAG, "Auto migration is not enabled! Skipping.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forced && !FeatureFlags.groupsV1ManualMigration()) {
|
||||||
|
warn(TAG, "Manual migration is not enabled! Skipping.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RecipientUtil.ensureUuidsAreAvailable(context, groupRecipient.getParticipants());
|
||||||
|
groupRecipient = groupRecipient.fresh();
|
||||||
|
|
||||||
|
List<Recipient> registeredMembers = RecipientUtil.getEligibleForSending(groupRecipient.getParticipants());
|
||||||
|
List<Recipient> possibleMembers = forced ? getMigratableManualMigrationMembers(registeredMembers)
|
||||||
|
: getMigratableAutoMigrationMembers(registeredMembers);
|
||||||
|
|
||||||
|
if (!forced && possibleMembers.size() != registeredMembers.size()) {
|
||||||
|
warn(TAG, "Not allowed to invite or leave registered users behind in an auto-migration! Skipping.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(TAG, "Attempting to create group.");
|
||||||
|
|
||||||
|
try {
|
||||||
|
GroupManager.migrateGroupToServer(context, gv1Id, possibleMembers);
|
||||||
|
newlyCreated = true;
|
||||||
|
log(TAG, "Successfully created!");
|
||||||
|
} catch (GroupChangeFailedException e) {
|
||||||
|
warn(TAG, "Failed to migrate group. Retrying.", e);
|
||||||
|
throw new RetryLaterException();
|
||||||
|
} catch (MembershipNotSuitableForV2Exception e) {
|
||||||
|
warn(TAG, "Failed to migrate job due to the membership not yet being suitable for GV2. Aborting.", e);
|
||||||
|
return;
|
||||||
|
} catch (GroupAlreadyExistsException e) {
|
||||||
|
warn(TAG, "Someone else created the group while we were trying to do the same! It exists now. Continuing on.", e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NOT_A_MEMBER:
|
||||||
|
warn(TAG, "The migrated group already exists, but we are not a member. Doing a local leave.");
|
||||||
|
handleLeftBehind(context, gv1Id, groupRecipient, threadId);
|
||||||
|
return;
|
||||||
|
case FULL_OR_PENDING_MEMBER:
|
||||||
|
warn(TAG, "The migrated group already exists, and we're in it. Continuing on.");
|
||||||
|
break;
|
||||||
|
default: throw new AssertionError();
|
||||||
|
}
|
||||||
|
|
||||||
|
log(TAG, "Migrating local group " + gv1Id + " to " + gv2Id);
|
||||||
|
|
||||||
|
DecryptedGroup decryptedGroup = performLocalMigration(context, gv1Id, threadId, groupRecipient);
|
||||||
|
|
||||||
|
if (newlyCreated && decryptedGroup != null && !SignalStore.internalValues().disableGv1AutoMigrateNotification()) {
|
||||||
|
GroupManager.sendNoopUpdate(context, gv2MasterKey, decryptedGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void performLocalMigration(@NonNull Context context, @NonNull GroupId.V1 gv1Id) throws IOException {
|
||||||
|
Recipient recipient = Recipient.externalGroupExact(context, gv1Id);
|
||||||
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||||
|
|
||||||
|
performLocalMigration(context, gv1Id, threadId, recipient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable DecryptedGroup performLocalMigration(@NonNull Context context, @NonNull GroupId.V1 gv1Id, long threadId, @NonNull Recipient groupRecipient) throws IOException {
|
||||||
|
DecryptedGroup decryptedGroup;
|
||||||
|
try {
|
||||||
|
decryptedGroup = GroupManager.addedGroupVersion(context, gv1Id.deriveV2MigrationMasterKey());
|
||||||
|
} catch (GroupDoesNotExistException e) {
|
||||||
|
throw new IOException("[Local] The group should exist already!");
|
||||||
|
} catch (GroupNotAMemberException e) {
|
||||||
|
Log.w(TAG, "[Local] We are not in the group. Doing a local leave.");
|
||||||
|
handleLeftBehind(context, gv1Id, groupRecipient, threadId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RecipientId> pendingRecipients = Stream.of(DecryptedGroupUtil.pendingToUuidList(decryptedGroup.getPendingMembersList()))
|
||||||
|
.map(uuid -> Recipient.externalPush(context, uuid, null, false))
|
||||||
|
.filterNot(Recipient::isSelf)
|
||||||
|
.map(Recipient::getId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Log.i(TAG, "[Local] Migrating group over to the version we were added to: V" + decryptedGroup.getRevision());
|
||||||
|
DatabaseFactory.getGroupDatabase(context).migrateToV2(gv1Id, decryptedGroup);
|
||||||
|
DatabaseFactory.getSmsDatabase(context).insertGroupV1MigrationEvent(groupRecipient.getId(), threadId, pendingRecipients);
|
||||||
|
|
||||||
|
Log.i(TAG, "[Local] Applying all changes since V" + decryptedGroup.getRevision());
|
||||||
|
try {
|
||||||
|
GroupManager.updateGroupFromServer(context, gv1Id.deriveV2MigrationMasterKey(), LATEST, System.currentTimeMillis(), null);
|
||||||
|
} catch (GroupChangeBusyException | GroupNotAMemberException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return decryptedGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleLeftBehind(@NonNull Context context, @NonNull GroupId.V1 gv1Id, @NonNull Recipient groupRecipient, long threadId) {
|
||||||
|
DatabaseFactory.getGroupDatabase(context).setActive(gv1Id, false);
|
||||||
|
|
||||||
|
OutgoingMediaMessage leaveMessage = GroupUtil.createGroupV1LeaveMessage(gv1Id, groupRecipient);
|
||||||
|
try {
|
||||||
|
long id = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(leaveMessage, threadId, false, null);
|
||||||
|
DatabaseFactory.getMmsDatabase(context).markAsSent(id, true);
|
||||||
|
} catch (MmsException e) {
|
||||||
|
Log.w(TAG, "Failed to insert group leave message!", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In addition to meeting traditional requirements, you must also have a profile key for a member
|
||||||
|
* to consider them migratable in an auto-migration.
|
||||||
|
*/
|
||||||
|
private static @NonNull List<Recipient> getMigratableAutoMigrationMembers(@NonNull List<Recipient> registeredMembers) {
|
||||||
|
return Stream.of(getMigratableManualMigrationMembers(registeredMembers))
|
||||||
|
.filter(r -> r.getProfileKey() != null)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can only migrate users that have the required capabilities.
|
||||||
|
*/
|
||||||
|
private static @NonNull List<Recipient> getMigratableManualMigrationMembers(@NonNull List<Recipient> registeredMembers) {
|
||||||
|
return Stream.of(registeredMembers)
|
||||||
|
.filter(r -> r.getGroupsV2Capability() == Recipient.Capability.SUPPORTED &&
|
||||||
|
r.getGroupsV1MigrationCapability() == Recipient.Capability.SUPPORTED)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||||
|
return e instanceof PushNetworkException ||
|
||||||
|
e instanceof NoCredentialForRedemptionTimeException ||
|
||||||
|
e instanceof GroupChangeBusyException ||
|
||||||
|
e instanceof RetryLaterException;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Factory implements Job.Factory<GroupV1MigrationJob> {
|
||||||
|
@Override
|
||||||
|
public @NonNull GroupV1MigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
return new GroupV1MigrationJob(parameters,
|
||||||
|
RecipientId.from(data.getString(KEY_RECIPIENT_ID)),
|
||||||
|
data.getBoolean(KEY_FORCED));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -69,6 +69,7 @@ public final class JobManagerFactories {
|
||||||
put(CreateSignedPreKeyJob.KEY, new CreateSignedPreKeyJob.Factory());
|
put(CreateSignedPreKeyJob.KEY, new CreateSignedPreKeyJob.Factory());
|
||||||
put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory());
|
put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory());
|
||||||
put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory());
|
put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory());
|
||||||
|
put(GroupV1MigrationJob.KEY, new GroupV1MigrationJob.Factory());
|
||||||
put(KbsEnclaveMigrationWorkerJob.KEY, new KbsEnclaveMigrationWorkerJob.Factory());
|
put(KbsEnclaveMigrationWorkerJob.KEY, new KbsEnclaveMigrationWorkerJob.Factory());
|
||||||
put(LeaveGroupJob.KEY, new LeaveGroupJob.Factory());
|
put(LeaveGroupJob.KEY, new LeaveGroupJob.Factory());
|
||||||
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
||||||
|
|
|
@ -24,7 +24,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||||
import org.thoughtcrime.securesms.util.Base64;
|
import org.thoughtcrime.securesms.util.Base64;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||||
|
@ -85,7 +84,7 @@ public final class PushGroupSilentUpdateSendJob extends BaseJob {
|
||||||
MessageGroupContext.GroupV2Properties properties = groupMessage.requireGroupV2Properties();
|
MessageGroupContext.GroupV2Properties properties = groupMessage.requireGroupV2Properties();
|
||||||
SignalServiceProtos.GroupContextV2 groupContext = properties.getGroupContext();
|
SignalServiceProtos.GroupContextV2 groupContext = properties.getGroupContext();
|
||||||
|
|
||||||
String queue = Recipient.externalGroup(context, groupId).getId().toQueueKey();
|
String queue = Recipient.externalGroupExact(context, groupId).getId().toQueueKey();
|
||||||
|
|
||||||
return new PushGroupSilentUpdateSendJob(new ArrayList<>(recipients),
|
return new PushGroupSilentUpdateSendJob(new ArrayList<>(recipients),
|
||||||
recipients.size(),
|
recipients.size(),
|
||||||
|
|
|
@ -236,15 +236,17 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||||
try {
|
try {
|
||||||
SignalServiceGroupContext signalServiceGroupContext = content.getDataMessage().get().getGroupContext().get();
|
SignalServiceGroupContext signalServiceGroupContext = content.getDataMessage().get().getGroupContext().get();
|
||||||
GroupId groupId = GroupUtil.idFromGroupContext(signalServiceGroupContext);
|
GroupId groupId = GroupUtil.idFromGroupContext(signalServiceGroupContext);
|
||||||
Recipient recipient = Recipient.externalGroup(context, groupId);
|
|
||||||
|
|
||||||
queueName = getQueueName(recipient.getId());
|
queueName = getQueueName(Recipient.externalPossiblyMigratedGroup(context, groupId).getId());
|
||||||
|
|
||||||
if (groupId.isV2()) {
|
if (groupId.isV2()) {
|
||||||
int localRevision = DatabaseFactory.getGroupDatabase(context)
|
int localRevision = DatabaseFactory.getGroupDatabase(context)
|
||||||
.getGroupV2Revision(groupId.requireV2());
|
.getGroupV2Revision(groupId.requireV2());
|
||||||
|
|
||||||
if (signalServiceGroupContext.getGroupV2().get().getRevision() > localRevision) {
|
if (signalServiceGroupContext.getGroupV2().get().getRevision() > localRevision ||
|
||||||
|
DatabaseFactory.getGroupDatabase(context).getGroupV1ByExpectedV2(groupId.requireV2()).isPresent())
|
||||||
|
{
|
||||||
|
Log.i(TAG, "Adding network constraint to group-related job.");
|
||||||
builder.addConstraint(NetworkConstraint.KEY)
|
builder.addConstraint(NetworkConstraint.KEY)
|
||||||
.setLifespan(TimeUnit.DAYS.toMillis(30));
|
.setLifespan(TimeUnit.DAYS.toMillis(30));
|
||||||
}
|
}
|
||||||
|
@ -256,7 +258,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||||
queueName = getQueueName(RecipientId.fromHighTrust(content.getSender()));
|
queueName = getQueueName(RecipientId.fromHighTrust(content.getSender()));
|
||||||
}
|
}
|
||||||
} else if (exceptionMetadata != null) {
|
} else if (exceptionMetadata != null) {
|
||||||
Recipient recipient = exceptionMetadata.groupId != null ? Recipient.externalGroup(context, exceptionMetadata.groupId)
|
Recipient recipient = exceptionMetadata.groupId != null ? Recipient.externalPossiblyMigratedGroup(context, exceptionMetadata.groupId)
|
||||||
: Recipient.external(context, exceptionMetadata.sender);
|
: Recipient.external(context, exceptionMetadata.sender);
|
||||||
queueName = getQueueName(recipient.getId());
|
queueName = getQueueName(recipient.getId());
|
||||||
}
|
}
|
||||||
|
@ -348,6 +350,11 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||||
if (isGv2Message) {
|
if (isGv2Message) {
|
||||||
GroupId.V2 groupIdV2 = groupId.get().requireV2();
|
GroupId.V2 groupIdV2 = groupId.get().requireV2();
|
||||||
|
|
||||||
|
Optional<GroupDatabase.GroupRecord> possibleGv1 = groupDatabase.getGroupV1ByExpectedV2(groupIdV2);
|
||||||
|
if (possibleGv1.isPresent()) {
|
||||||
|
GroupV1MigrationJob.performLocalMigration(context, possibleGv1.get().getId().requireV1());
|
||||||
|
}
|
||||||
|
|
||||||
if (!updateGv2GroupFromServerOrP2PChange(content, message.getGroupContext().get().getGroupV2().get())) {
|
if (!updateGv2GroupFromServerOrP2PChange(content, message.getGroupContext().get().getGroupV2().get())) {
|
||||||
log(TAG, String.valueOf(content.getTimestamp()), "Ignoring GV2 message for group we are not currently in " + groupIdV2);
|
log(TAG, String.valueOf(content.getTimestamp()), "Ignoring GV2 message for group we are not currently in " + groupIdV2);
|
||||||
return;
|
return;
|
||||||
|
@ -877,7 +884,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||||
recipient = Recipient.externalPush(context, response.getPerson().get());
|
recipient = Recipient.externalPush(context, response.getPerson().get());
|
||||||
} else if (response.getGroupId().isPresent()) {
|
} else if (response.getGroupId().isPresent()) {
|
||||||
GroupId groupId = GroupId.v1(response.getGroupId().get());
|
GroupId groupId = GroupId.v1(response.getGroupId().get());
|
||||||
recipient = Recipient.externalGroup(context, groupId);
|
recipient = Recipient.externalPossiblyMigratedGroup(context, groupId);
|
||||||
} else {
|
} else {
|
||||||
warn(TAG, "Message request response was missing a thread recipient! Skipping.");
|
warn(TAG, "Message request response was missing a thread recipient! Skipping.");
|
||||||
return;
|
return;
|
||||||
|
@ -1323,7 +1330,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||||
boolean isGroup = recipient.isGroup();
|
boolean isGroup = recipient.isGroup();
|
||||||
|
|
||||||
MessageDatabase database;
|
MessageDatabase database;
|
||||||
long messageId;
|
long messageId;
|
||||||
|
|
||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient,
|
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient,
|
||||||
|
@ -1552,7 +1559,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
|
Recipient groupRecipient = Recipient.externalPossiblyMigratedGroup(context, groupId);
|
||||||
|
|
||||||
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1779,7 +1786,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||||
throws BadGroupIdException
|
throws BadGroupIdException
|
||||||
{
|
{
|
||||||
if (message.isPresent()) {
|
if (message.isPresent()) {
|
||||||
return Optional.of(Recipient.externalGroup(context, GroupUtil.idFromGroupContext(message.get())));
|
return Optional.of(Recipient.externalPossiblyMigratedGroup(context, GroupUtil.idFromGroupContext(message.get())));
|
||||||
}
|
}
|
||||||
return Optional.absent();
|
return Optional.absent();
|
||||||
}
|
}
|
||||||
|
@ -1814,6 +1821,15 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||||
Optional<GroupId> groupId = GroupUtil.idFromGroupContext(message.getGroupContext());
|
Optional<GroupId> groupId = GroupUtil.idFromGroupContext(message.getGroupContext());
|
||||||
|
|
||||||
|
if (groupId.isPresent() &&
|
||||||
|
groupId.get().isV1() &&
|
||||||
|
message.isGroupV1Update() &&
|
||||||
|
groupDatabase.groupExists(groupId.get().requireV1().deriveV2MigrationGroupId()))
|
||||||
|
{
|
||||||
|
warn(TAG, String.valueOf(content.getTimestamp()), "Ignoring V1 update for a group we've already migrated to V2.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
|
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
|
||||||
return sender.isBlocked();
|
return sender.isBlocked();
|
||||||
}
|
}
|
||||||
|
@ -1839,7 +1855,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||||
|
|
||||||
if (content.getTypingMessage().get().getGroupId().isPresent()) {
|
if (content.getTypingMessage().get().getGroupId().isPresent()) {
|
||||||
GroupId groupId = GroupId.push(content.getTypingMessage().get().getGroupId().get());
|
GroupId groupId = GroupId.push(content.getTypingMessage().get().getGroupId().get());
|
||||||
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
|
Recipient groupRecipient = Recipient.externalPossiblyMigratedGroup(context, groupId);
|
||||||
return groupRecipient.isBlocked() || !groupRecipient.isActiveGroup();
|
return groupRecipient.isBlocked() || !groupRecipient.isActiveGroup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ final class RequestGroupV2InfoWorkerJob extends BaseJob {
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
RequestGroupV2InfoWorkerJob(@NonNull GroupId.V2 groupId, int toRevision) {
|
RequestGroupV2InfoWorkerJob(@NonNull GroupId.V2 groupId, int toRevision) {
|
||||||
this(new Parameters.Builder()
|
this(new Parameters.Builder()
|
||||||
.setQueue(PushProcessMessageJob.getQueueName(Recipient.externalGroup(ApplicationDependencies.getApplication(), groupId).getId()))
|
.setQueue(PushProcessMessageJob.getQueueName(Recipient.externalGroupExact(ApplicationDependencies.getApplication(), groupId).getId()))
|
||||||
.addConstraint(NetworkConstraint.KEY)
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||||
.setMaxAttempts(Parameters.UNLIMITED)
|
.setMaxAttempts(Parameters.UNLIMITED)
|
||||||
|
|
|
@ -4,12 +4,14 @@ import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
|
|
||||||
public final class InternalValues extends SignalStoreValues {
|
public final class InternalValues extends SignalStoreValues {
|
||||||
|
|
||||||
public static final String GV2_DO_NOT_CREATE_GV2 = "internal.gv2.do_not_create_gv2";
|
public static final String GV2_DO_NOT_CREATE_GV2 = "internal.gv2.do_not_create_gv2";
|
||||||
public static final String GV2_FORCE_INVITES = "internal.gv2.force_invites";
|
public static final String GV2_FORCE_INVITES = "internal.gv2.force_invites";
|
||||||
public static final String GV2_IGNORE_SERVER_CHANGES = "internal.gv2.ignore_server_changes";
|
public static final String GV2_IGNORE_SERVER_CHANGES = "internal.gv2.ignore_server_changes";
|
||||||
public static final String GV2_IGNORE_P2P_CHANGES = "internal.gv2.ignore_p2p_changes";
|
public static final String GV2_IGNORE_P2P_CHANGES = "internal.gv2.ignore_p2p_changes";
|
||||||
public static final String RECIPIENT_DETAILS = "internal.recipient_details";
|
public static final String GV2_DISABLE_AUTOMIGRATE_INITIATION = "internal.gv2.disable_automigrate_initiation";
|
||||||
public static final String FORCE_CENSORSHIP = "internal.force_censorship";
|
public static final String GV2_DISABLE_AUTOMIGRATE_NOTIFICATION = "internal.gv2.disable_automigrate_notification";
|
||||||
|
public static final String RECIPIENT_DETAILS = "internal.recipient_details";
|
||||||
|
public static final String FORCE_CENSORSHIP = "internal.force_censorship";
|
||||||
|
|
||||||
InternalValues(KeyValueStore store) {
|
InternalValues(KeyValueStore store) {
|
||||||
super(store);
|
super(store);
|
||||||
|
@ -67,4 +69,20 @@ public final class InternalValues extends SignalStoreValues {
|
||||||
public synchronized boolean forcedCensorship() {
|
public synchronized boolean forcedCensorship() {
|
||||||
return FeatureFlags.internalUser() && getBoolean(FORCE_CENSORSHIP, false);
|
return FeatureFlags.internalUser() && getBoolean(FORCE_CENSORSHIP, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable initiating a GV1->GV2 auto-migration. You can still recognize a group has been
|
||||||
|
* auto-migrated.
|
||||||
|
*/
|
||||||
|
public synchronized boolean disableGv1AutoMigrateInitiation() {
|
||||||
|
return FeatureFlags.internalUser() && getBoolean(GV2_DISABLE_AUTOMIGRATE_INITIATION, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable sending a group update after an automigration. This will force other group members to
|
||||||
|
* have to discover the migration on their own.
|
||||||
|
*/
|
||||||
|
public synchronized boolean disableGv1AutoMigrateNotification() {
|
||||||
|
return FeatureFlags.internalUser() && getBoolean(GV2_DISABLE_AUTOMIGRATE_NOTIFICATION, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
package org.thoughtcrime.securesms.keyvalue;
|
package org.thoughtcrime.securesms.keyvalue;
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
public final class MiscellaneousValues extends SignalStoreValues {
|
public final class MiscellaneousValues extends SignalStoreValues {
|
||||||
|
|
||||||
private static final String LAST_PREKEY_REFRESH_TIME = "last_prekey_refresh_time";
|
private static final String LAST_PREKEY_REFRESH_TIME = "last_prekey_refresh_time";
|
||||||
private static final String MESSAGE_REQUEST_ENABLE_TIME = "message_request_enable_time";
|
private static final String MESSAGE_REQUEST_ENABLE_TIME = "message_request_enable_time";
|
||||||
private static final String LAST_PROFILE_REFRESH_TIME = "misc.last_profile_refresh_time";
|
private static final String LAST_PROFILE_REFRESH_TIME = "misc.last_profile_refresh_time";
|
||||||
private static final String USERNAME_SHOW_REMINDER = "username.show.reminder";
|
private static final String LAST_GV1_ROUTINE_MIGRATION_TIME = "misc.last_gv1_routine_migration_time";
|
||||||
private static final String CLIENT_DEPRECATED = "misc.client_deprecated";
|
private static final String USERNAME_SHOW_REMINDER = "username.show.reminder";
|
||||||
|
private static final String CLIENT_DEPRECATED = "misc.client_deprecated";
|
||||||
|
|
||||||
MiscellaneousValues(@NonNull KeyValueStore store) {
|
MiscellaneousValues(@NonNull KeyValueStore store) {
|
||||||
super(store);
|
super(store);
|
||||||
|
@ -43,6 +40,14 @@ public final class MiscellaneousValues extends SignalStoreValues {
|
||||||
putLong(LAST_PROFILE_REFRESH_TIME, time);
|
putLong(LAST_PROFILE_REFRESH_TIME, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getLastGv1RoutineMigrationTime() {
|
||||||
|
return getLong(LAST_GV1_ROUTINE_MIGRATION_TIME, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastGv1RoutineMigrationTime(long time) {
|
||||||
|
putLong(LAST_GV1_ROUTINE_MIGRATION_TIME, time);
|
||||||
|
}
|
||||||
|
|
||||||
public void hideUsernameReminder() {
|
public void hideUsernameReminder() {
|
||||||
putBoolean(USERNAME_SHOW_REMINDER, false);
|
putBoolean(USERNAME_SHOW_REMINDER, false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,8 @@ public class InternalOptionsPreferenceFragment extends CorrectedPreferenceFragme
|
||||||
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_FORCE_INVITES, SignalStore.internalValues().gv2ForceInvites());
|
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_FORCE_INVITES, SignalStore.internalValues().gv2ForceInvites());
|
||||||
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_IGNORE_SERVER_CHANGES, SignalStore.internalValues().gv2IgnoreServerChanges());
|
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_IGNORE_SERVER_CHANGES, SignalStore.internalValues().gv2IgnoreServerChanges());
|
||||||
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_IGNORE_P2P_CHANGES, SignalStore.internalValues().gv2IgnoreP2PChanges());
|
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_IGNORE_P2P_CHANGES, SignalStore.internalValues().gv2IgnoreP2PChanges());
|
||||||
|
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_DISABLE_AUTOMIGRATE_INITIATION, SignalStore.internalValues().disableGv1AutoMigrateInitiation());
|
||||||
|
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_DISABLE_AUTOMIGRATE_NOTIFICATION, SignalStore.internalValues().disableGv1AutoMigrateNotification());
|
||||||
initializeSwitchPreference(preferenceDataStore, InternalValues.FORCE_CENSORSHIP, SignalStore.internalValues().forcedCensorship());
|
initializeSwitchPreference(preferenceDataStore, InternalValues.FORCE_CENSORSHIP, SignalStore.internalValues().forcedCensorship());
|
||||||
|
|
||||||
findPreference("pref_refresh_attributes").setOnPreferenceClickListener(preference -> {
|
findPreference("pref_refresh_attributes").setOnPreferenceClickListener(preference -> {
|
||||||
|
|
|
@ -94,7 +94,7 @@ class EditPushGroupProfileRepository implements EditProfileRepository {
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private RecipientId getRecipientId() {
|
private RecipientId getRecipientId() {
|
||||||
return DatabaseFactory.getRecipientDatabase(context).getByGroupId(groupId.toString())
|
return DatabaseFactory.getRecipientDatabase(context).getByGroupId(groupId)
|
||||||
.or(() -> {
|
.or(() -> {
|
||||||
throw new AssertionError("Recipient ID for Group ID does not exist.");
|
throw new AssertionError("Recipient ID for Group ID does not exist.");
|
||||||
});
|
});
|
||||||
|
|
|
@ -234,12 +234,32 @@ public class Recipient {
|
||||||
/**
|
/**
|
||||||
* A version of {@link #external(Context, String)} that should be used when you know the
|
* A version of {@link #external(Context, String)} that should be used when you know the
|
||||||
* identifier is a groupId.
|
* identifier is a groupId.
|
||||||
|
*
|
||||||
|
* Important: This will throw an exception if the groupId you're using could have been migrated.
|
||||||
|
* If you're dealing with inbound data, you should be using
|
||||||
|
* {@link #externalPossiblyMigratedGroup(Context, GroupId)}, or checking the database before
|
||||||
|
* calling this method.
|
||||||
*/
|
*/
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public static @NonNull Recipient externalGroup(@NonNull Context context, @NonNull GroupId groupId) {
|
public static @NonNull Recipient externalGroupExact(@NonNull Context context, @NonNull GroupId groupId) {
|
||||||
return Recipient.resolved(DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId));
|
return Recipient.resolved(DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will give you one of:
|
||||||
|
* - The recipient that matches the groupId specified exactly
|
||||||
|
* - The recipient whose V1 ID would map to the provided V2 ID
|
||||||
|
* - The recipient whose V2 ID would be derived from the provided V1 ID
|
||||||
|
* - A newly-created recipient for the provided ID if none of the above match
|
||||||
|
*
|
||||||
|
* Important: You could get back a recipient with a different groupId than the one you provided.
|
||||||
|
* You should be very cautious when using the groupId on the returned recipient.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
public static @NonNull Recipient externalPossiblyMigratedGroup(@NonNull Context context, @NonNull GroupId groupId) {
|
||||||
|
return Recipient.resolved(DatabaseFactory.getRecipientDatabase(context).getOrInsertFromPossiblyMigratedGroupId(groupId));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a fully-populated {@link Recipient} based off of a string identifier, creating one in
|
* Returns a fully-populated {@link Recipient} based off of a string identifier, creating one in
|
||||||
* the database if necessary. The identifier may be a uuid, phone number, email,
|
* the database if necessary. The identifier may be a uuid, phone number, email,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.json.JSONObject;
|
||||||
import org.thoughtcrime.securesms.BuildConfig;
|
import org.thoughtcrime.securesms.BuildConfig;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||||
|
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob;
|
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
@ -61,6 +62,8 @@ public final class FeatureFlags {
|
||||||
public static final String MODERN_PROFILE_SHARING = "android.modernProfileSharing";
|
public static final String MODERN_PROFILE_SHARING = "android.modernProfileSharing";
|
||||||
private static final String VIEWED_RECEIPTS = "android.viewed.receipts";
|
private static final String VIEWED_RECEIPTS = "android.viewed.receipts";
|
||||||
private static final String MAX_ENVELOPE_SIZE = "android.maxEnvelopeSize";
|
private static final String MAX_ENVELOPE_SIZE = "android.maxEnvelopeSize";
|
||||||
|
private static final String GV1_AUTO_MIGRATE_VERSION = "android.groupsv2.autoMigrateVersion";
|
||||||
|
private static final String GV1_MANUAL_MIGRATE_VERSION = "android.groupsv2.manualMigrateVersion";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||||
|
@ -79,7 +82,9 @@ public final class FeatureFlags {
|
||||||
RESEARCH_MEGAPHONE_1,
|
RESEARCH_MEGAPHONE_1,
|
||||||
MODERN_PROFILE_SHARING,
|
MODERN_PROFILE_SHARING,
|
||||||
VIEWED_RECEIPTS,
|
VIEWED_RECEIPTS,
|
||||||
MAX_ENVELOPE_SIZE
|
MAX_ENVELOPE_SIZE,
|
||||||
|
GV1_AUTO_MIGRATE_VERSION,
|
||||||
|
GV1_MANUAL_MIGRATE_VERSION
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -124,6 +129,7 @@ public final class FeatureFlags {
|
||||||
* desired test state.
|
* desired test state.
|
||||||
*/
|
*/
|
||||||
private static final Map<String, OnFlagChange> FLAG_CHANGE_LISTENERS = new HashMap<String, OnFlagChange>() {{
|
private static final Map<String, OnFlagChange> FLAG_CHANGE_LISTENERS = new HashMap<String, OnFlagChange>() {{
|
||||||
|
put(GV1_AUTO_MIGRATE_VERSION, change -> ApplicationDependencies.getJobManager().add(new RefreshAttributesJob()));
|
||||||
}};
|
}};
|
||||||
|
|
||||||
private static final Map<String, Object> REMOTE_VALUES = new TreeMap<>();
|
private static final Map<String, Object> REMOTE_VALUES = new TreeMap<>();
|
||||||
|
@ -257,6 +263,16 @@ public final class FeatureFlags {
|
||||||
return getInteger(MAX_ENVELOPE_SIZE, 0);
|
return getInteger(MAX_ENVELOPE_SIZE, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether or not auto-migration from GV1->GV2 is enabled. */
|
||||||
|
public static boolean groupsV1AutoMigration() {
|
||||||
|
return getVersionFlag(GV1_AUTO_MIGRATE_VERSION) == VersionFlag.ON;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether or not manual migration from GV1->GV2 is enabled. */
|
||||||
|
public static boolean groupsV1ManualMigration() {
|
||||||
|
return groupsV1AutoMigration() && getVersionFlag(GV1_MANUAL_MIGRATE_VERSION) == VersionFlag.ON;
|
||||||
|
}
|
||||||
|
|
||||||
/** Only for rendering debug info. */
|
/** Only for rendering debug info. */
|
||||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||||
return new TreeMap<>(REMOTE_VALUES);
|
return new TreeMap<>(REMOTE_VALUES);
|
||||||
|
|
|
@ -6,6 +6,8 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
import org.signal.zkgroup.InvalidInputException;
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
@ -15,6 +17,7 @@ import org.thoughtcrime.securesms.groups.BadGroupIdException;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.MessageGroupContext;
|
import org.thoughtcrime.securesms.mms.MessageGroupContext;
|
||||||
|
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
@ -22,8 +25,11 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public final class GroupUtil {
|
public final class GroupUtil {
|
||||||
|
@ -108,6 +114,26 @@ public final class GroupUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static OutgoingGroupUpdateMessage createGroupV1LeaveMessage(@NonNull GroupId.V1 groupId,
|
||||||
|
@NonNull Recipient groupRecipient)
|
||||||
|
{
|
||||||
|
GroupContext groupContext = GroupContext.newBuilder()
|
||||||
|
.setId(ByteString.copyFrom(groupId.getDecodedId()))
|
||||||
|
.setType(GroupContext.Type.QUIT)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new OutgoingGroupUpdateMessage(groupRecipient,
|
||||||
|
groupContext,
|
||||||
|
null,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
Collections.emptyList(),
|
||||||
|
Collections.emptyList(),
|
||||||
|
Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
public static class GroupDescription {
|
public static class GroupDescription {
|
||||||
|
|
||||||
@NonNull private final Context context;
|
@NonNull private final Context context;
|
||||||
|
|
|
@ -2258,6 +2258,7 @@
|
||||||
<!-- Internal only preferences -->
|
<!-- Internal only preferences -->
|
||||||
<string name="preferences__internal_preferences" translatable="false">Internal Preferences</string>
|
<string name="preferences__internal_preferences" translatable="false">Internal Preferences</string>
|
||||||
<string name="preferences__internal_preferences_groups_v2" translatable="false">Groups V2</string>
|
<string name="preferences__internal_preferences_groups_v2" translatable="false">Groups V2</string>
|
||||||
|
<string name="preferences__internal_preferences_groups_v1_migration" translatable="false">Groups V1 Migration</string>
|
||||||
<string name="preferences__internal_do_not_create_gv2" translatable="false">Do not create GV2 groups</string>
|
<string name="preferences__internal_do_not_create_gv2" translatable="false">Do not create GV2 groups</string>
|
||||||
<string name="preferences__internal_do_not_create_gv2_description" translatable="false">Do not attempt to create GV2 groups, i.e. will force creation of GV1 or MMS groups.</string>
|
<string name="preferences__internal_do_not_create_gv2_description" translatable="false">Do not attempt to create GV2 groups, i.e. will force creation of GV1 or MMS groups.</string>
|
||||||
<string name="preferences__internal_force_gv2_invites" translatable="false">Force Invites</string>
|
<string name="preferences__internal_force_gv2_invites" translatable="false">Force Invites</string>
|
||||||
|
@ -2266,6 +2267,10 @@
|
||||||
<string name="preferences__internal_ignore_gv2_server_changes_description" translatable="false">Changes in server\'s response will be ignored, causing passive voice update messages if P2P is also ignored.</string>
|
<string name="preferences__internal_ignore_gv2_server_changes_description" translatable="false">Changes in server\'s response will be ignored, causing passive voice update messages if P2P is also ignored.</string>
|
||||||
<string name="preferences__internal_ignore_gv2_p2p_changes" translatable="false">Ignore P2P changes</string>
|
<string name="preferences__internal_ignore_gv2_p2p_changes" translatable="false">Ignore P2P changes</string>
|
||||||
<string name="preferences__internal_ignore_gv2_p2p_changes_description" translatable="false">Changes sent P2P will be ignored. In conjunction with ignoring server changes, will cause passive voice.</string>
|
<string name="preferences__internal_ignore_gv2_p2p_changes_description" translatable="false">Changes sent P2P will be ignored. In conjunction with ignoring server changes, will cause passive voice.</string>
|
||||||
|
<string name="preferences__internal_do_not_initiate_automigrate" translatable="false">Disable Auto-Migration Initiation</string>
|
||||||
|
<string name="preferences__internal_do_not_initiate_automigrate_description" translatable="false">Do not attempt to initiate an auto-migration. You will still recognize migrated groups.</string>
|
||||||
|
<string name="preferences__internal_do_not_notify_automigrate" translatable="false">Disable Auto-Migration Notification</string>
|
||||||
|
<string name="preferences__internal_do_not_notify_automigrate_description" translatable="false">Do not attempt to notify other users of an auto-migration. They will have to discover it on their own.</string>
|
||||||
<string name="preferences__internal_account" translatable="false">Account</string>
|
<string name="preferences__internal_account" translatable="false">Account</string>
|
||||||
<string name="preferences__internal_refresh_attributes" translatable="false">Refresh attributes</string>
|
<string name="preferences__internal_refresh_attributes" translatable="false">Refresh attributes</string>
|
||||||
<string name="preferences__internal_refresh_attributes_description" translatable="false">Forces a write of capabilities on to the server followed by a read.</string>
|
<string name="preferences__internal_refresh_attributes_description" translatable="false">Forces a write of capabilities on to the server followed by a read.</string>
|
||||||
|
|
|
@ -75,6 +75,24 @@
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
android:key="internal_groupsV1Migration"
|
||||||
|
android:title="@string/preferences__internal_preferences_groups_v1_migration">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="internal.gv2.disable_automigrate_initiation"
|
||||||
|
android:summary="@string/preferences__internal_do_not_initiate_automigrate_description"
|
||||||
|
android:title="@string/preferences__internal_do_not_initiate_automigrate" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="internal.gv2.disable_automigrate_notification"
|
||||||
|
android:summary="@string/preferences__internal_do_not_notify_automigrate_description"
|
||||||
|
android:title="@string/preferences__internal_do_not_notify_automigrate" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:title="@string/preferences__internal_network">
|
android:title="@string/preferences__internal_network">
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue