Remove GV1 migration support.

This commit is contained in:
Cody Henthorne
2023-11-17 14:07:21 -05:00
committed by Greyson Parrelli
parent 213d996168
commit 34a228f85e
30 changed files with 42 additions and 928 deletions

View File

@@ -32,8 +32,4 @@ sealed class ConversationSettingsEvent {
class ShowMembersAdded(
val membersAddedCount: Int
) : ConversationSettingsEvent()
class InitiateGroupMigration(
val recipientId: RecipientId
) : ConversationSettingsEvent()
}

View File

@@ -72,7 +72,6 @@ import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndR
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentDialog
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreBottomSheetDialogFragment
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
@@ -279,7 +278,6 @@ class ConversationSettingsFragment : DSLSettingsFragment(
is ConversationSettingsEvent.ShowAddMembersToGroupError -> showAddMembersToGroupError(event)
is ConversationSettingsEvent.ShowGroupInvitesSentDialog -> showGroupInvitesSentDialog(event)
is ConversationSettingsEvent.ShowMembersAdded -> showMembersAdded(event)
is ConversationSettingsEvent.InitiateGroupMigration -> GroupsV1MigrationInitiationBottomSheetDialogFragment.showForInitiation(parentFragmentManager, event.recipientId)
}
}
}
@@ -363,7 +361,6 @@ class ConversationSettingsFragment : DSLSettingsFragment(
LegacyGroupPreference.Model(
state = groupState.legacyGroupState,
onLearnMoreClick = { GroupsLearnMoreBottomSheetDialogFragment.show(parentFragmentManager) },
onUpgradeClick = { viewModel.initiateGroupUpgrade() },
onMmsWarningClick = { startActivity(Intent(requireContext(), InviteActivity::class.java)) }
)
)

View File

@@ -30,7 +30,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.RecipientUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
import org.thoughtcrime.securesms.util.livedata.Store
@@ -119,8 +118,6 @@ sealed class ConversationSettingsViewModel(
}
}
open fun initiateGroupUpgrade(): Unit = error("This ViewModel does not support this interaction")
private class RecipientSettingsViewModel(
private val recipientId: RecipientId,
private val callMessageIds: LongArray,
@@ -281,7 +278,7 @@ sealed class ConversationSettingsViewModel(
),
canModifyBlockedState = RecipientUtil.isBlockable(recipient),
specificSettingsState = state.requireGroupSettingsState().copy(
legacyGroupState = getLegacyGroupState(recipient)
legacyGroupState = getLegacyGroupState()
)
)
}
@@ -390,14 +387,8 @@ sealed class ConversationSettingsViewModel(
}
}
private fun getLegacyGroupState(recipient: Recipient): LegacyGroupPreference.State {
val showLegacyInfo = recipient.requireGroupId().isV1
return if (showLegacyInfo && recipient.participantIds.size > FeatureFlags.groupLimits().hardLimit) {
LegacyGroupPreference.State.TOO_LARGE
} else if (showLegacyInfo) {
LegacyGroupPreference.State.UPGRADE
} else if (groupId.isMms) {
private fun getLegacyGroupState(): LegacyGroupPreference.State {
return if (groupId.isMms) {
LegacyGroupPreference.State.MMS_WARNING
} else {
LegacyGroupPreference.State.NONE
@@ -468,12 +459,6 @@ sealed class ConversationSettingsViewModel(
override fun unblock() {
repository.unblock(groupId)
}
override fun initiateGroupUpgrade() {
repository.getExternalPossiblyMigratedGroupRecipientId(groupId) {
internalEvents.onNext(ConversationSettingsEvent.InitiateGroupMigration(it))
}
}
}
class Factory(

View File

@@ -4,7 +4,6 @@ import android.view.View
import androidx.core.content.ContextCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
@@ -19,7 +18,6 @@ object LegacyGroupPreference {
class Model(
val state: State,
val onLearnMoreClick: () -> Unit,
val onUpgradeClick: () -> Unit,
val onMmsWarningClick: () -> Unit
) : PreferenceModel<Model>() {
override fun areItemsTheSame(newItem: Model): Boolean {
@@ -42,15 +40,6 @@ object LegacyGroupPreference {
groupInfoText.setOnLinkClickListener { model.onLearnMoreClick() }
groupInfoText.setLearnMoreVisible(true)
}
State.UPGRADE -> {
groupInfoText.setText(R.string.ManageGroupActivity_legacy_group_upgrade)
groupInfoText.setOnLinkClickListener { model.onUpgradeClick() }
groupInfoText.setLearnMoreVisible(true, R.string.ManageGroupActivity_upgrade_this_group)
}
State.TOO_LARGE -> {
groupInfoText.text = context.getString(R.string.ManageGroupActivity_legacy_group_too_large, FeatureFlags.groupLimits().hardLimit - 1)
groupInfoText.setLearnMoreVisible(false)
}
State.MMS_WARNING -> {
groupInfoText.setText(R.string.ManageGroupActivity_this_is_an_insecure_mms_group)
groupInfoText.setOnLinkClickListener { model.onMmsWarningClick() }
@@ -63,8 +52,6 @@ object LegacyGroupPreference {
enum class State {
LEARN_MORE,
UPGRADE,
TOO_LARGE,
MMS_WARNING,
NONE
}

View File

@@ -16,7 +16,6 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
@@ -119,14 +118,6 @@ internal object ConversationOptionsMenu {
}
}
menuInflater.inflate(R.menu.conversation_group_options, menu)
if (!recipient.isPushGroup) {
menuInflater.inflate(R.menu.conversation_mms_group_options, menu)
if (distributionType == ThreadTable.DistributionTypes.BROADCAST) {
menu.findItem(R.id.menu_distribution_broadcast).isChecked = true
} else {
menu.findItem(R.id.menu_distribution_conversation).isChecked = true
}
}
menuInflater.inflate(R.menu.conversation_active_group_options, menu)
}
@@ -216,8 +207,6 @@ internal object ConversationOptionsMenu {
R.id.menu_search -> callback.handleSearch()
R.id.menu_add_to_contacts -> callback.handleAddToContacts()
R.id.menu_group_recipients -> callback.handleDisplayGroupRecipients()
R.id.menu_distribution_broadcast -> callback.handleDistributionBroadcastEnabled(menuItem)
R.id.menu_distribution_conversation -> callback.handleDistributionConversationEnabled(menuItem)
R.id.menu_group_settings -> callback.handleManageGroup()
R.id.menu_leave -> callback.handleLeavePushGroup()
R.id.menu_invite -> callback.handleInviteLink()
@@ -283,8 +272,6 @@ internal object ConversationOptionsMenu {
fun handleSearch()
fun handleAddToContacts()
fun handleDisplayGroupRecipients()
fun handleDistributionBroadcastEnabled(menuItem: MenuItem)
fun handleDistributionConversationEnabled(menuItem: MenuItem)
fun handleManageGroup()
fun handleLeavePushGroup()
fun handleInviteLink()

View File

@@ -219,7 +219,6 @@ import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndR
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInfoBottomSheetDialogFragment
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestionsDialog
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult
import org.thoughtcrime.securesms.invites.InviteActions
@@ -3144,10 +3143,6 @@ class ConversationFragment :
GroupMembersDialog(requireActivity(), recipientSnapshot).display()
}
override fun handleDistributionBroadcastEnabled(menuItem: MenuItem) = error("This fragment does not support this action.")
override fun handleDistributionConversationEnabled(menuItem: MenuItem) = error("This fragment does not support this action.")
override fun handleManageGroup() {
val recipient = viewModel.recipientSnapshot ?: return
val intent = ConversationSettingsActivity.forGroup(requireContext(), recipient.requireGroupId())
@@ -3612,16 +3607,6 @@ class ConversationFragment :
}
}
override fun onGroupV1MigrationClicked() {
val recipient = viewModel.recipientSnapshot
if (recipient == null) {
Log.w(TAG, "[onGroupV1MigrationClicked] No recipient!")
return
}
GroupsV1MigrationInitiationBottomSheetDialogFragment.showForInitiation(childFragmentManager, recipient.id)
}
override fun onInviteToSignal(recipient: Recipient) {
InviteActions.inviteUserToSignal(
context = requireContext(),

View File

@@ -87,7 +87,6 @@ class DisabledInputView @JvmOverloads constructor(
setDeleteOnClickListener { listener?.onDeleteGroupClicked() }
setBlockOnClickListener { listener?.onBlockClicked() }
setUnblockOnClickListener { listener?.onUnblockClicked() }
setGroupV1MigrationContinueListener { listener?.onGroupV1MigrationClicked() }
}
)
}
@@ -230,7 +229,6 @@ class DisabledInputView @JvmOverloads constructor(
fun onDeleteGroupClicked()
fun onBlockClicked()
fun onUnblockClicked()
fun onGroupV1MigrationClicked()
fun onInviteToSignal(recipient: Recipient)
fun onUnmuteReleaseNotesChannel()
}

View File

@@ -44,7 +44,6 @@ import org.thoughtcrime.securesms.groups.BadGroupIdException
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupId.Push
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
import org.thoughtcrime.securesms.groups.GroupsV1MigratedCache
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -658,15 +657,9 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
@JvmOverloads
@CheckReturnValue
fun create(groupMasterKey: GroupMasterKey, groupState: DecryptedGroup, force: Boolean = false): GroupId.V2? {
fun create(groupMasterKey: GroupMasterKey, groupState: DecryptedGroup): GroupId.V2? {
val groupId = GroupId.v2(groupMasterKey)
if (!force && GroupsV1MigratedCache.hasV1Group(groupId)) {
throw MissedGroupMigrationInsertException(groupId)
} else if (force) {
Log.w(TAG, "Forcing the creation of a group even though we already have a V1 ID!")
}
return if (create(groupId = groupId, title = groupState.title, memberCollection = emptyList(), avatar = null, groupMasterKey = groupMasterKey, groupState = groupState)) {
groupId
} else {
@@ -680,9 +673,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
*/
fun fixMissingMasterKey(groupMasterKey: GroupMasterKey) {
val groupId = GroupId.v2(groupMasterKey)
if (GroupsV1MigratedCache.hasV1Group(groupId)) {
Log.w(TAG, "There already exists a V1 group that should be migrated into this group. But if the recipient already exists, there's not much we can do here.")
}
writableDatabase.withinTransaction { db ->
val updated = db
@@ -697,8 +687,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
groupMasterKey,
DecryptedGroup.Builder()
.revision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
.build(),
true
.build()
)
} else {
Log.w(TAG, "Had a group entry, but it was missing a master key. Updated.")
@@ -1526,5 +1515,4 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
}
class LegacyGroupInsertException(id: GroupId?) : IllegalStateException("Tried to create a new GV1 entry when we already had a migrated GV2! $id")
class MissedGroupMigrationInsertException(id: GroupId?) : IllegalStateException("Tried to create a new GV2 entry when we already had a V1 group that mapped to the new ID! $id")
}

View File

@@ -56,7 +56,6 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors.Id.Companion.fo
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper.getChatColors
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.GroupTable.LegacyGroupInsertException
import org.thoughtcrime.securesms.database.GroupTable.MissedGroupMigrationInsertException
import org.thoughtcrime.securesms.database.GroupTable.ShowAsStoryState
import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups
@@ -80,7 +79,6 @@ import org.thoughtcrime.securesms.groups.BadGroupIdException
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupId.V1
import org.thoughtcrime.securesms.groups.GroupId.V2
import org.thoughtcrime.securesms.groups.GroupsV1MigratedCache
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
@@ -574,8 +572,6 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
return existing.get()
} else if (groupId.isV1 && groups.groupExists(groupId.requireV1().deriveV2MigrationGroupId())) {
throw LegacyGroupInsertException(groupId)
} else if (groupId.isV2 && GroupsV1MigratedCache.hasV1Group(groupId.requireV2())) {
throw MissedGroupMigrationInsertException(groupId)
} else {
val values = ContentValues().apply {
put(GROUP_ID, groupId.toString())
@@ -589,8 +585,6 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
return existing.get()
} else if (groupId.isV1 && groups.groupExists(groupId.requireV1().deriveV2MigrationGroupId())) {
throw LegacyGroupInsertException(groupId)
} else if (groupId.isV2 && groups.getGroupV1ByExpectedV2(groupId.requireV2()).isPresent) {
throw MissedGroupMigrationInsertException(groupId)
} else {
throw AssertionError("Failed to insert recipient!")
}
@@ -644,14 +638,6 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
}
}
if (groupId.isV2) {
val v1 = GroupsV1MigratedCache.getV1GroupByV2Id(groupId.requireV2())
if (v1 != null) {
db.setTransactionSuccessful()
return v1.recipientId
}
}
val id = getOrInsertFromGroupId(groupId)
db.setTransactionSuccessful()
return id

View File

@@ -5,9 +5,9 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.AccessControl
import org.signal.storageservice.protos.groups.local.EnabledState
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.groups.GroupAccessControl
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -134,7 +134,7 @@ class GroupRecord(
unmigratedV1Members
.filterNot { members.contains(it) }
.map { Recipient.resolved(it) }
.filter { GroupsV1MigrationUtil.isAutoMigratable(it) }
.filter { it.isAutoMigratable() }
.map { it.id }
}
}
@@ -181,4 +181,13 @@ class GroupRecord(
}
return false
}
companion object {
/**
* True if the user meets all the requirements to be auto-migrated, otherwise false.
*/
private fun Recipient.isAutoMigratable(): Boolean {
return hasServiceId() && registered === RecipientTable.RegisteredState.REGISTERED && profileKey != null
}
}
}

View File

@@ -937,13 +937,6 @@ final class GroupManagerV2 {
alreadyAMember = true;
}
GroupRecord unmigratedV1Group = GroupsV1MigratedCache.getV1GroupByV2Id(groupId);
if (unmigratedV1Group != null) {
Log.i(TAG, "Group link was for a migrated V1 group we know about! Migrating it and using that as the base.");
GroupsV1MigrationUtil.performLocalMigration(context, unmigratedV1Group.getId().requireV1());
}
DecryptedGroup decryptedGroup = createPlaceholderGroup(joinInfo, requestToJoin);
Optional<GroupRecord> group = groupDatabase.getGroup(groupId);

View File

@@ -1,46 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.groups
import androidx.annotation.WorkerThread
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.util.LRUCache
/**
* Cache to keep track of groups we know do not need a migration run on. This is to save time looking for a gv1 group
* with the expected v2 id.
*/
object GroupsV1MigratedCache {
private const val MAX_CACHE = 1000
private val noV1GroupCache = LRUCache<GroupId.V2, Boolean>(MAX_CACHE)
@JvmStatic
@WorkerThread
fun hasV1Group(groupId: GroupId.V2): Boolean {
return getV1GroupByV2Id(groupId) != null
}
@JvmStatic
@WorkerThread
fun getV1GroupByV2Id(groupId: GroupId.V2): GroupRecord? {
synchronized(noV1GroupCache) {
if (noV1GroupCache.containsKey(groupId)) {
return null
}
}
val v1Group = SignalDatabase.groups.getGroupV1ByExpectedV2(groupId)
if (!v1Group.isPresent) {
synchronized(noV1GroupCache) {
noV1GroupCache.put(groupId, true)
}
}
return v1Group.orNull()
}
}

View File

@@ -1,216 +0,0 @@
package org.thoughtcrime.securesms.groups;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.thoughtcrime.securesms.database.GroupTable;
import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
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 java.io.Closeable;
import java.io.IOException;
import java.util.List;
import static org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor.LATEST;
public final class GroupsV1MigrationUtil {
private static final String TAG = Log.tag(GroupsV1MigrationUtil.class);
private GroupsV1MigrationUtil() {}
public static void migrate(@NonNull Context context, @NonNull RecipientId recipientId, boolean forced)
throws IOException, RetryLaterException, GroupChangeBusyException, InvalidMigrationStateException
{
Recipient groupRecipient = Recipient.resolved(recipientId);
Long threadId = SignalDatabase.threads().getThreadIdFor(recipientId);
GroupTable groupDatabase = SignalDatabase.groups();
if (threadId == null) {
Log.w(TAG, "No thread found!");
throw new InvalidMigrationStateException();
}
if (!groupRecipient.isPushV1Group()) {
Log.w(TAG, "Not a V1 group!");
throw new InvalidMigrationStateException();
}
if (groupRecipient.getParticipantIds().size() > FeatureFlags.groupLimits().getHardLimit()) {
Log.w(TAG, "Too many members! Size: " + groupRecipient.getParticipantIds().size());
throw new InvalidMigrationStateException();
}
GroupId.V1 gv1Id = groupRecipient.requireGroupId().requireV1();
GroupId.V2 gv2Id = gv1Id.deriveV2MigrationGroupId();
GroupMasterKey gv2MasterKey = gv1Id.deriveV2MigrationMasterKey();
boolean newlyCreated = false;
if (groupDatabase.groupExists(gv2Id)) {
Log.w(TAG, "We already have a V2 group for this V1 group! Must have been added before we were migration-capable.");
throw new InvalidMigrationStateException();
}
if (!groupRecipient.isActiveGroup()) {
Log.w(TAG, "Group is inactive! Can't migrate.");
throw new InvalidMigrationStateException();
}
switch (GroupManager.v2GroupStatus(context, SignalStore.account().requireAci(), gv2MasterKey)) {
case DOES_NOT_EXIST:
Log.i(TAG, "Group does not exist on the service.");
if (!groupRecipient.isProfileSharing()) {
Log.w(TAG, "Profile sharing is disabled! Can't migrate.");
throw new InvalidMigrationStateException();
}
List<Recipient> registeredMembers = RecipientUtil.getEligibleForSending(Recipient.resolvedList(groupRecipient.getParticipantIds()));
if (RecipientUtil.ensureUuidsAreAvailable(context, registeredMembers)) {
Log.i(TAG, "Newly-discovered UUIDs. Getting fresh recipients.");
registeredMembers = Stream.of(registeredMembers).map(Recipient::fresh).toList();
}
List<Recipient> possibleMembers = forced ? registeredMembers
: getMigratableAutoMigrationMembers(registeredMembers);
if (!forced && !groupRecipient.hasName()) {
Log.w(TAG, "Group has no name. Skipping auto-migration.");
throw new InvalidMigrationStateException();
}
if (!forced && possibleMembers.size() != registeredMembers.size()) {
Log.w(TAG, "Not allowed to invite or leave registered users behind in an auto-migration! Skipping.");
throw new InvalidMigrationStateException();
}
Log.i(TAG, "Attempting to create group.");
try {
GroupManager.migrateGroupToServer(context, gv1Id, possibleMembers);
newlyCreated = true;
Log.i(TAG, "Successfully created!");
} catch (GroupChangeFailedException e) {
Log.w(TAG, "Failed to migrate group. Retrying.", e);
throw new RetryLaterException();
} catch (MembershipNotSuitableForV2Exception e) {
Log.w(TAG, "Failed to migrate job due to the membership not yet being suitable for GV2. Aborting.", e);
return;
} catch (GroupAlreadyExistsException e) {
Log.w(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:
Log.w(TAG, "The migrated group already exists, but we are not a member. Doing a local leave.");
handleLeftBehind(gv1Id);
return;
case FULL_OR_PENDING_MEMBER:
Log.w(TAG, "The migrated group already exists, and we're in it. Continuing on.");
break;
default: throw new AssertionError();
}
Log.i(TAG, "Migrating local group " + gv1Id + " to " + gv2Id);
DecryptedGroup decryptedGroup = performLocalMigration(context, gv1Id, threadId, groupRecipient);
if (newlyCreated && decryptedGroup != null) {
Log.i(TAG, "Sending no-op update to notify others.");
GroupManager.sendNoopUpdate(context, gv2MasterKey, decryptedGroup);
}
}
public static void performLocalMigration(@NonNull Context context, @NonNull GroupId.V1 gv1Id) throws IOException
{
Log.i(TAG, "Beginning local migration! V1 ID: " + gv1Id, new Throwable());
try (Closeable ignored = GroupsV2ProcessingLock.acquireGroupProcessingLock(1000)) {
if (SignalDatabase.groups().groupExists(gv1Id.deriveV2MigrationGroupId())) {
Log.w(TAG, "Group was already migrated! Could have been waiting for the lock.", new Throwable());
return;
}
Recipient recipient = Recipient.externalGroupExact(gv1Id);
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
performLocalMigration(context, gv1Id, threadId, recipient);
Log.i(TAG, "Migration complete! (" + gv1Id + ", " + threadId + ", " + recipient.getId() + ")", new Throwable());
} catch (GroupChangeBusyException e) {
throw new IOException(e);
}
}
private static @Nullable DecryptedGroup performLocalMigration(@NonNull Context context,
@NonNull GroupId.V1 gv1Id,
long threadId,
@NonNull Recipient groupRecipient)
throws IOException, GroupChangeBusyException
{
Log.i(TAG, "performLocalMigration(" + gv1Id + ", " + threadId + ", " + groupRecipient.getId());
try (Closeable ignored = GroupsV2ProcessingLock.acquireGroupProcessingLock()){
DecryptedGroup decryptedGroup;
try {
decryptedGroup = GroupManager.addedGroupVersion(SignalStore.account().requireAci(), 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(gv1Id);
return null;
}
Log.i(TAG, "[Local] Migrating group over to the version we were added to: V" + decryptedGroup.revision);
SignalDatabase.groups().migrateToV2(threadId, gv1Id, decryptedGroup);
Log.i(TAG, "[Local] Applying all changes since V" + decryptedGroup.revision);
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 GroupId.V1 gv1Id) {
SignalDatabase.groups().setActive(gv1Id, false);
SignalDatabase.groups().remove(gv1Id, Recipient.self().getId());
}
/**
* 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(registeredMembers)
.filter(r -> r.getProfileKey() != null)
.toList();
}
/**
* True if the user meets all the requirements to be auto-migrated, otherwise false.
*/
public static boolean isAutoMigratable(@NonNull Recipient recipient) {
return recipient.hasServiceId() &&
recipient.getRegistered() == RecipientTable.RegisteredState.REGISTERED &&
recipient.getProfileKey() != null;
}
public static final class InvalidMigrationStateException extends Exception {
}
}

View File

@@ -25,8 +25,7 @@ import org.thoughtcrime.securesms.util.WindowUtil;
import java.util.List;
/**
* Shows more info about a GV1->GV2 migration event. Looks similar to
* {@link GroupsV1MigrationInitiationBottomSheetDialogFragment}, but only displays static data.
* Shows more info about a GV1->GV2 migration event.
*/
public final class GroupsV1MigrationInfoBottomSheetDialogFragment extends BottomSheetDialogFragment {

View File

@@ -1,151 +0,0 @@
package org.thoughtcrime.securesms.groups.ui.migration;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
/**
* A bottom sheet that allows a user to initiation a manual GV1->GV2 migration. Will show the user
* the members that will be invited/left behind.
*/
public final class GroupsV1MigrationInitiationBottomSheetDialogFragment extends BottomSheetDialogFragment {
private static final String KEY_GROUP_RECIPIENT_ID = "group_recipient_id";
private GroupsV1MigrationInitiationViewModel viewModel;
private GroupMemberListView inviteList;
private TextView inviteTitle;
private View inviteContainer;
private GroupMemberListView ineligibleList;
private TextView ineligibleTitle;
private View ineligibleContainer;
private View upgradeButton;
private View spinner;
public static void showForInitiation(@NonNull FragmentManager manager, @NonNull RecipientId groupRecipientId) {
Bundle args = new Bundle();
args.putParcelable(KEY_GROUP_RECIPIENT_ID, groupRecipientId);
GroupsV1MigrationInitiationBottomSheetDialogFragment fragment = new GroupsV1MigrationInitiationBottomSheetDialogFragment();
fragment.setArguments(args);
fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
setStyle(DialogFragment.STYLE_NORMAL,
ThemeUtil.isDarkTheme(requireContext()) ? R.style.Theme_Signal_RoundedBottomSheet
: R.style.Theme_Signal_RoundedBottomSheet_Light);
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.groupsv1_migration_bottom_sheet, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
this.inviteContainer = view.findViewById(R.id.gv1_migrate_invite_container);
this.inviteTitle = view.findViewById(R.id.gv1_migrate_invite_title);
this.inviteList = view.findViewById(R.id.gv1_migrate_invite_list);
this.ineligibleContainer = view.findViewById(R.id.gv1_migrate_ineligible_container);
this.ineligibleTitle = view.findViewById(R.id.gv1_migrate_ineligible_title);
this.ineligibleList = view.findViewById(R.id.gv1_migrate_ineligible_list);
this.upgradeButton = view.findViewById(R.id.gv1_migrate_upgrade_button);
this.spinner = view.findViewById(R.id.gv1_migrate_spinner);
inviteList.initializeAdapter(getViewLifecycleOwner());
ineligibleList.initializeAdapter(getViewLifecycleOwner());
inviteList.setNestedScrollingEnabled(false);
ineligibleList.setNestedScrollingEnabled(false);
//noinspection ConstantConditions
RecipientId groupRecipientId = getArguments().getParcelable(KEY_GROUP_RECIPIENT_ID);
//noinspection ConstantConditions
viewModel = new ViewModelProvider(this, new GroupsV1MigrationInitiationViewModel.Factory(groupRecipientId)).get(GroupsV1MigrationInitiationViewModel.class);
viewModel.getMigrationState().observe(getViewLifecycleOwner(), this::onMigrationStateChanged);
upgradeButton.setEnabled(false);
upgradeButton.setOnClickListener(v -> onUpgradeClicked());
view.findViewById(R.id.gv1_migrate_cancel_button).setOnClickListener(v -> dismiss());
}
@Override
public void onResume() {
super.onResume();
WindowUtil.initializeScreenshotSecurity(requireContext(), requireDialog().getWindow());
}
@Override
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
BottomSheetUtil.show(manager, tag, this);
}
private void onMigrationStateChanged(@NonNull MigrationState migrationState) {
if (migrationState.getNeedsInvite().size() > 0) {
inviteContainer.setVisibility(View.VISIBLE);
inviteTitle.setText(getResources().getQuantityText(R.plurals.GroupsV1MigrationInitiation_these_members_will_need_to_accept_an_invite, migrationState.getNeedsInvite().size()));
inviteList.setDisplayOnlyMembers(migrationState.getNeedsInvite());
} else {
inviteContainer.setVisibility(View.GONE);
}
if (migrationState.getIneligible().size() > 0) {
ineligibleContainer.setVisibility(View.VISIBLE);
ineligibleTitle.setText(getResources().getQuantityText(R.plurals.GroupsV1MigrationInitiation_these_members_are_not_capable_of_joining_new_groups, migrationState.getIneligible().size()));
ineligibleList.setDisplayOnlyMembers(migrationState.getIneligible());
} else {
ineligibleContainer.setVisibility(View.GONE);
}
upgradeButton.setEnabled(true);
spinner.setVisibility(View.GONE);
}
private void onUpgradeClicked() {
AlertDialog dialog = SimpleProgressDialog.show(requireContext());
viewModel.onUpgradeClicked().observe(getViewLifecycleOwner(), result -> {
switch (result) {
case SUCCESS:
dismiss();
break;
case FAILURE_GENERAL:
Toast.makeText(requireContext(), R.string.GroupsV1MigrationInitiation_failed_to_upgrade, Toast.LENGTH_SHORT).show();
dismiss();
break;
case FAILURE_NETWORK:
Toast.makeText(requireContext(), R.string.GroupsV1MigrationInitiation_encountered_a_network_error, Toast.LENGTH_SHORT).show();
dismiss();
break;
default:
throw new IllegalStateException();
}
dialog.dismiss();
});
}
}

View File

@@ -1,50 +0,0 @@
package org.thoughtcrime.securesms.groups.ui.migration;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.recipients.RecipientId;
class GroupsV1MigrationInitiationViewModel extends ViewModel {
private final RecipientId groupRecipientId;
private final MutableLiveData<MigrationState> migrationState;
private final GroupsV1MigrationRepository repository;
private GroupsV1MigrationInitiationViewModel(@NonNull RecipientId groupRecipientId) {
this.groupRecipientId = groupRecipientId;
this.migrationState = new MutableLiveData<>();
this.repository = new GroupsV1MigrationRepository();
repository.getMigrationState(groupRecipientId, migrationState::postValue);
}
@NonNull LiveData<MigrationState> getMigrationState() {
return migrationState;
}
@NonNull LiveData<MigrationResult> onUpgradeClicked() {
MutableLiveData <MigrationResult> migrationResult = new MutableLiveData<>();
repository.upgradeGroup(groupRecipientId, migrationResult::postValue);
return migrationResult;
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final RecipientId groupRecipientId;
Factory(@NonNull RecipientId groupRecipientId) {
this.groupRecipientId = groupRecipientId;
}
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return modelClass.cast(new GroupsV1MigrationInitiationViewModel(groupRecipientId));
}
}
}

View File

@@ -1,92 +0,0 @@
package org.thoughtcrime.securesms.groups.ui.migration;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer;
import com.annimon.stream.Stream;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
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 java.io.IOException;
import java.util.Collections;
import java.util.List;
final class GroupsV1MigrationRepository {
private static final String TAG = Log.tag(GroupsV1MigrationRepository.class);
void getMigrationState(@NonNull RecipientId groupRecipientId, @NonNull Consumer<MigrationState> callback) {
SignalExecutors.BOUNDED.execute(() -> callback.accept(getMigrationState(groupRecipientId)));
}
void upgradeGroup(@NonNull RecipientId recipientId, @NonNull Consumer<MigrationResult> callback) {
SignalExecutors.UNBOUNDED.execute(() -> {
if (!NetworkConstraint.isMet(ApplicationDependencies.getApplication())) {
Log.w(TAG, "No network!");
callback.accept(MigrationResult.FAILURE_NETWORK);
return;
}
if (!Recipient.resolved(recipientId).isPushV1Group()) {
Log.w(TAG, "Not a V1 group!");
callback.accept(MigrationResult.FAILURE_GENERAL);
return;
}
try {
GroupsV1MigrationUtil.migrate(ApplicationDependencies.getApplication(), recipientId, true);
callback.accept(MigrationResult.SUCCESS);
} catch (IOException | RetryLaterException | GroupChangeBusyException e) {
callback.accept(MigrationResult.FAILURE_NETWORK);
} catch (GroupsV1MigrationUtil.InvalidMigrationStateException e) {
callback.accept(MigrationResult.FAILURE_GENERAL);
}
});
}
@WorkerThread
private MigrationState getMigrationState(@NonNull RecipientId groupRecipientId) {
Recipient group = Recipient.resolved(groupRecipientId);
if (!group.isPushV1Group()) {
return new MigrationState(Collections.emptyList(), Collections.emptyList());
}
List<Recipient> members = Recipient.resolvedList(group.getParticipantIds());
try {
List<Recipient> registered = Stream.of(members)
.filter(Recipient::isRegistered)
.toList();
RecipientUtil.ensureUuidsAreAvailable(ApplicationDependencies.getApplication(), registered);
} catch (IOException e) {
Log.w(TAG, "Failed to refresh UUIDs!", e);
}
group = group.fresh();
List<Recipient> ineligible = Stream.of(members)
.filter(r -> !r.hasServiceId() || r.getRegistered() != RecipientTable.RegisteredState.REGISTERED)
.toList();
List<Recipient> invites = Stream.of(members)
.filterNot(ineligible::contains)
.filterNot(Recipient::isSelf)
.filter(r -> r.getProfileKey() == null)
.toList();
return new MigrationState(invites, ineligible);
}
}

View File

@@ -1,5 +0,0 @@
package org.thoughtcrime.securesms.groups.ui.migration;
enum MigrationResult {
SUCCESS, FAILURE_GENERAL, FAILURE_NETWORK
}

View File

@@ -1,30 +0,0 @@
package org.thoughtcrime.securesms.groups.ui.migration;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.recipients.Recipient;
import java.util.List;
/**
* Represents the migration state of a group. Namely, which users will be invited or left behind.
*/
final class MigrationState {
private final List<Recipient> needsInvite;
private final List<Recipient> ineligible;
MigrationState(@NonNull List<Recipient> needsInvite,
@NonNull List<Recipient> ineligible)
{
this.needsInvite = needsInvite;
this.ineligible = ineligible;
}
public @NonNull List<Recipient> getNeedsInvite() {
return needsInvite;
}
public @NonNull List<Recipient> getIneligible() {
return ineligible;
}
}

View File

@@ -1,103 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
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 int ROUTINE_LIMIT = 20;
private static final long REFRESH_INTERVAL = TimeUnit.HOURS.toMillis(1);
private final RecipientId recipientId;
private GroupV1MigrationJob(@NonNull RecipientId recipientId) {
this(new Parameters.Builder()
.setQueue(recipientId.toQueueKey())
.setMaxAttempts(Parameters.UNLIMITED)
.setLifespan(TimeUnit.DAYS.toMillis(7))
.addConstraint(NetworkConstraint.KEY)
.build(),
recipientId);
}
private GroupV1MigrationJob(@NonNull Parameters parameters, @NonNull RecipientId recipientId) {
super(parameters);
this.recipientId = recipientId;
}
public static void enqueuePossibleAutoMigrate(@NonNull RecipientId recipientId) {
SignalExecutors.BOUNDED.execute(() -> {
if (Recipient.resolved(recipientId).isPushV1Group()) {
ApplicationDependencies.getJobManager().add(new GroupV1MigrationJob(recipientId));
}
});
}
@Override
public @Nullable byte[] serialize() {
return new JsonJobData.Builder().putString(KEY_RECIPIENT_ID, recipientId.serialize())
.serialize();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
protected void onRun() throws IOException, GroupChangeBusyException, RetryLaterException {
if (Recipient.resolved(recipientId).isBlocked()) {
Log.i(TAG, "Group blocked. Skipping.");
return;
}
try {
GroupsV1MigrationUtil.migrate(context, recipientId, false);
} catch (GroupsV1MigrationUtil.InvalidMigrationStateException e) {
Log.w(TAG, "Invalid migration state. Skipping.");
}
}
@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, @Nullable byte[] serializedData) {
JsonJobData data = JsonJobData.deserialize(serializedData);
return new GroupV1MigrationJob(parameters, RecipientId.from(data.getString(KEY_RECIPIENT_ID)));
}
}
}

View File

@@ -124,7 +124,6 @@ public final class JobManagerFactories {
put(ForceUpdateGroupV2WorkerJob.KEY, new ForceUpdateGroupV2WorkerJob.Factory());
put(GenerateAudioWaveFormJob.KEY, new GenerateAudioWaveFormJob.Factory());
put(GiftSendJob.KEY, new GiftSendJob.Factory());
put(GroupV1MigrationJob.KEY, new GroupV1MigrationJob.Factory());
put(GroupCallUpdateSendJob.KEY, new GroupCallUpdateSendJob.Factory());
put(GroupCallPeekJob.KEY, new GroupCallPeekJob.Factory());
put(GroupCallPeekWorkerJob.KEY, new GroupCallPeekWorkerJob.Factory());
@@ -305,6 +304,7 @@ public final class JobManagerFactories {
put("MmsDownloadJob", new FailingJob.Factory());
put("SmsReceiveJob", new FailingJob.Factory());
put("StoryReadStateMigrationJob", new PassingMigrationJob.Factory());
put("GroupV1MigrationJob", new FailingJob.Factory());
}};
}

View File

@@ -6,7 +6,6 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupChangeBusyException
import org.thoughtcrime.securesms.groups.GroupsV1MigratedCache
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.ChangeNumberConstraint
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
@@ -122,7 +121,7 @@ class PushProcessMessageJob private constructor(
if (groupId.isV2) {
val localRevision = groups.getGroupV2Revision(groupId.requireV2())
if (groupContext.revision!! > localRevision || GroupsV1MigratedCache.hasV1Group(groupId)) {
if (groupContext.revision!! > localRevision) {
Log.i(TAG, "Adding network constraint to group-related job.")
requireNetwork = true
}

View File

@@ -126,21 +126,17 @@ public final class MessageRequestRepository {
}
} else if (!RecipientUtil.isLegacyProfileSharingAccepted(recipient) && isLegacyThread(recipient)) {
if (recipient.isGroup()) {
return MessageRequestState.LEGACY_GROUP_V1;
return MessageRequestState.DEPRECATED_GROUP_V1;
} else {
return MessageRequestState.LEGACY_INDIVIDUAL;
}
} else if (recipient.isPushV1Group()) {
if (RecipientUtil.isMessageRequestAccepted(context, threadId)) {
if (recipient.getParticipantIds().size() > FeatureFlags.groupLimits().getHardLimit()) {
return MessageRequestState.DEPRECATED_GROUP_V1_TOO_LARGE;
} else {
return MessageRequestState.DEPRECATED_GROUP_V1;
}
return MessageRequestState.DEPRECATED_GROUP_V1;
} else if (!recipient.isActiveGroup()) {
return MessageRequestState.NONE;
} else {
return MessageRequestState.GROUP_V1;
return MessageRequestState.DEPRECATED_GROUP_V1;
}
} else {
if (RecipientUtil.isMessageRequestAccepted(context, threadId)) {

View File

@@ -19,18 +19,9 @@ public enum MessageRequestState {
/** An individual conversation that existed pre-message-requests but doesn't have profile sharing enabled */
LEGACY_INDIVIDUAL,
/** A V1 group conversation that existed pre-message-requests but doesn't have profile sharing enabled */
LEGACY_GROUP_V1,
/** A V1 group conversation that is no longer allowed, because we've forced GV2 on. */
DEPRECATED_GROUP_V1,
/** A V1 group conversation that is no longer allowed, because we've forced GV2 on, but it's also too large to migrate. Nothing we can do. */
DEPRECATED_GROUP_V1_TOO_LARGE,
/** A message request is needed for a V1 group */
GROUP_V1,
/** An invite response is needed for a V2 group */
GROUP_V2_INVITE,

View File

@@ -4,9 +4,9 @@ import android.content.Context;
import android.content.res.ColorStateList;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.Group;
import androidx.core.text.HtmlCompat;
@@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.HtmlUtil;
import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
@@ -29,7 +28,6 @@ public class MessageRequestsBottomView extends ConstraintLayout {
private LearnMoreTextView question;
private MaterialButton accept;
private MaterialButton gv1Continue;
private MaterialButton block;
private MaterialButton delete;
private MaterialButton bigDelete;
@@ -38,8 +36,7 @@ public class MessageRequestsBottomView extends ConstraintLayout {
private Group normalButtons;
private Group blockedButtons;
private Group gv1MigrationButtons;
private Group activeGroup;
private @Nullable Group activeGroup;
public MessageRequestsBottomView(Context context) {
super(context);
@@ -66,10 +63,8 @@ public class MessageRequestsBottomView extends ConstraintLayout {
delete = findViewById(R.id.message_request_delete);
bigDelete = findViewById(R.id.message_request_big_delete);
bigUnblock = findViewById(R.id.message_request_big_unblock);
gv1Continue = findViewById(R.id.message_request_gv1_migration);
normalButtons = findViewById(R.id.message_request_normal_buttons);
blockedButtons = findViewById(R.id.message_request_blocked_buttons);
gv1MigrationButtons = findViewById(R.id.message_request_gv1_migration_buttons);
busyIndicator = findViewById(R.id.message_request_busy_indicator);
setWallpaperEnabled(false);
@@ -89,67 +84,49 @@ public class MessageRequestsBottomView extends ConstraintLayout {
question.setText(HtmlCompat.fromHtml(getContext().getString(message,
HtmlUtil.bold(recipient.getShortDisplayName(getContext()))), 0));
setActiveInactiveGroups(blockedButtons, normalButtons, gv1MigrationButtons);
setActiveInactiveGroups(blockedButtons, normalButtons);
break;
case BLOCKED_GROUP:
question.setText(R.string.MessageRequestBottomView_unblock_this_group_and_share_your_name_and_photo_with_its_members);
setActiveInactiveGroups(blockedButtons, normalButtons, gv1MigrationButtons);
setActiveInactiveGroups(blockedButtons, normalButtons);
break;
case LEGACY_INDIVIDUAL:
question.setText(getContext().getString(R.string.MessageRequestBottomView_continue_your_conversation_with_s_and_share_your_name_and_photo, recipient.getShortDisplayName(getContext())));
question.setLearnMoreVisible(true);
question.setOnLinkClickListener(v -> CommunicationActions.openBrowserLink(getContext(), getContext().getString(R.string.MessageRequestBottomView_legacy_learn_more_url)));
setActiveInactiveGroups(normalButtons, blockedButtons, gv1MigrationButtons);
accept.setText(R.string.MessageRequestBottomView_continue);
break;
case LEGACY_GROUP_V1:
question.setText(R.string.MessageRequestBottomView_continue_your_conversation_with_this_group_and_share_your_name_and_photo);
question.setLearnMoreVisible(true);
question.setOnLinkClickListener(v -> CommunicationActions.openBrowserLink(getContext(), getContext().getString(R.string.MessageRequestBottomView_legacy_learn_more_url)));
setActiveInactiveGroups(normalButtons, blockedButtons, gv1MigrationButtons);
setActiveInactiveGroups(normalButtons, blockedButtons);
accept.setText(R.string.MessageRequestBottomView_continue);
break;
case DEPRECATED_GROUP_V1:
question.setText(R.string.MessageRequestBottomView_upgrade_this_group_to_activate_new_features);
setActiveInactiveGroups(gv1MigrationButtons, normalButtons, blockedButtons);
gv1Continue.setVisibility(VISIBLE);
break;
case DEPRECATED_GROUP_V1_TOO_LARGE:
question.setText(getContext().getString(R.string.MessageRequestBottomView_this_legacy_group_can_no_longer_be_used, FeatureFlags.groupLimits().getHardLimit() - 1));
setActiveInactiveGroups(gv1MigrationButtons, normalButtons, blockedButtons);
gv1Continue.setVisibility(GONE);
break;
case GROUP_V1:
question.setText(R.string.MessageRequestBottomView_do_you_want_to_join_this_group_they_wont_know_youve_seen_their_messages_until_you_accept);
setActiveInactiveGroups(normalButtons, blockedButtons, gv1MigrationButtons);
accept.setText(R.string.MessageRequestBottomView_accept);
setActiveInactiveGroups(null, normalButtons, blockedButtons);
break;
case GROUP_V2_INVITE:
question.setText(R.string.MessageRequestBottomView_do_you_want_to_join_this_group_you_wont_see_their_messages);
setActiveInactiveGroups(normalButtons, blockedButtons, gv1MigrationButtons);
setActiveInactiveGroups(normalButtons, blockedButtons);
accept.setText(R.string.MessageRequestBottomView_accept);
break;
case GROUP_V2_ADD:
question.setText(R.string.MessageRequestBottomView_join_this_group_they_wont_know_youve_seen_their_messages_until_you_accept);
setActiveInactiveGroups(normalButtons, blockedButtons, gv1MigrationButtons);
setActiveInactiveGroups(normalButtons, blockedButtons);
accept.setText(R.string.MessageRequestBottomView_accept);
break;
case INDIVIDUAL:
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_they_wont_know_youve_seen_their_messages_until_you_accept,
HtmlUtil.bold(recipient.getShortDisplayName(getContext()))), 0));
setActiveInactiveGroups(normalButtons, blockedButtons, gv1MigrationButtons);
setActiveInactiveGroups(normalButtons, blockedButtons);
accept.setText(R.string.MessageRequestBottomView_accept);
break;
case INDIVIDUAL_HIDDEN:
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_you_removed_them_before,
HtmlUtil.bold(recipient.getShortDisplayName(getContext()))), 0));
setActiveInactiveGroups(normalButtons, blockedButtons, gv1MigrationButtons);
setActiveInactiveGroups(normalButtons, blockedButtons);
accept.setText(R.string.MessageRequestBottomView_accept);
break;
}
}
private void setActiveInactiveGroups(@NonNull Group activeGroup, @NonNull Group... inActiveGroups) {
private void setActiveInactiveGroups(@Nullable Group activeGroup, @NonNull Group... inActiveGroups) {
int initialVisibility = this.activeGroup != null ? this.activeGroup.getVisibility() : VISIBLE;
this.activeGroup = activeGroup;
@@ -158,7 +135,9 @@ public class MessageRequestsBottomView extends ConstraintLayout {
inactive.setVisibility(GONE);
}
activeGroup.setVisibility(initialVisibility);
if (activeGroup != null) {
activeGroup.setVisibility(initialVisibility);
}
}
public void showBusy() {
@@ -179,7 +158,7 @@ public class MessageRequestsBottomView extends ConstraintLayout {
public void setWallpaperEnabled(boolean isEnabled) {
MessageRequestBarColorTheme theme = MessageRequestBarColorTheme.resolveTheme(isEnabled);
Stream.of(delete, bigDelete, block, bigUnblock, accept, gv1Continue).forEach(button -> {
Stream.of(delete, bigDelete, block, bigUnblock, accept).forEach(button -> {
button.setBackgroundTintList(ColorStateList.valueOf(theme.getButtonBackgroundColor(getContext())));
});
@@ -187,7 +166,7 @@ public class MessageRequestsBottomView extends ConstraintLayout {
button.setTextColor(theme.getButtonForegroundDenyColor(getContext()));
});
Stream.of(accept, bigUnblock, gv1Continue).forEach(button -> {
Stream.of(accept, bigUnblock).forEach(button -> {
button.setTextColor(theme.getButtonForegroundAcceptColor(getContext()));
});
@@ -210,8 +189,4 @@ public class MessageRequestsBottomView extends ConstraintLayout {
public void setUnblockOnClickListener(OnClickListener unblockOnClickListener) {
bigUnblock.setOnClickListener(unblockOnClickListener);
}
public void setGroupV1MigrationContinueListener(OnClickListener acceptOnClickListener) {
gv1Continue.setOnClickListener(acceptOnClickListener);
}
}

View File

@@ -20,8 +20,6 @@ import org.thoughtcrime.securesms.groups.GroupChangeBusyException
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.groups.GroupNotAMemberException
import org.thoughtcrime.securesms.groups.GroupsV1MigratedCache
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
import org.thoughtcrime.securesms.jobs.AutomaticSessionResetJob
import org.thoughtcrime.securesms.jobs.NullMessageSendJob
@@ -232,10 +230,6 @@ open class MessageContentProcessor(private val context: Context) {
senderRecipient: Recipient,
groupSecretParams: GroupSecretParams? = null
): Gv2PreProcessResult {
val v1Group = GroupsV1MigratedCache.getV1GroupByV2Id(groupId)
if (v1Group != null) {
GroupsV1MigrationUtil.performLocalMigration(context, v1Group.id.requireV1())
}
val preUpdateGroupRecord = SignalDatabase.groups.getGroup(groupId)
val groupUpdateResult = updateGv2GroupFromServerOrP2PChange(context, timestamp, groupV2, preUpdateGroupRecord, groupSecretParams)
if (groupUpdateResult == null) {

View File

@@ -11,12 +11,10 @@ import org.thoughtcrime.securesms.database.GroupTable;
import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
@@ -98,26 +96,9 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
}
}
/**
* This contains a pretty big compromise: In the event that the new GV2 group we learned about
* was, in fact, a migrated V1 group we already knew about, we handle the migration here. This
* isn't great because the migration will likely result in network activity. And because this is
* all happening in a transaction, this could keep the transaction open for longer than we'd like.
* However, given that nearly all V1 groups have already been migrated, we're at a point where
* this event should be extraordinarily rare, and it didn't seem worth it to add a lot of
* complexity to accommodate this specific scenario.
*/
@Override
void insertLocal(@NonNull SignalGroupV2Record record) throws IOException {
GroupId.V2 actualV2Id = GroupId.v2(record.getMasterKeyOrThrow());
GroupId.V1 possibleV1Id = gv1GroupsByExpectedGv2Id.get(actualV2Id);
if (possibleV1Id != null) {
Log.i(TAG, "Discovered a new GV2 ID that is actually a migrated V1 group! Migrating now.");
GroupsV1MigrationUtil.performLocalMigration(context, possibleV1Id);
} else {
recipientTable.applyStorageSyncGroupV2Insert(record);
}
void insertLocal(@NonNull SignalGroupV2Record record) {
recipientTable.applyStorageSyncGroupV2Insert(record);
}
@Override

View File

@@ -66,19 +66,6 @@
app:layout_constraintStart_toEndOf="@+id/message_request_delete"
app:layout_constraintTop_toTopOf="@id/message_request_block" />
<com.google.android.material.button.MaterialButton
android:id="@+id/message_request_gv1_migration"
style="@style/Signal.Widget.Button.Medium.Tonal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
android:text="@string/MessageRequestBottomView_continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/message_request_big_delete"
style="@style/Signal.Widget.Button.Medium.Tonal"
@@ -109,7 +96,7 @@
android:layout_width="0dp"
android:layout_height="0dp"
app:barrierDirection="top"
app:constraint_referenced_ids="message_request_block,message_request_big_delete,message_request_gv1_migration" />
app:constraint_referenced_ids="message_request_block,message_request_big_delete" />
<androidx.constraintlayout.widget.Group
android:id="@+id/message_request_normal_buttons"
@@ -127,14 +114,6 @@
app:constraint_referenced_ids="message_request_big_delete,message_request_big_unblock"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Group
android:id="@+id/message_request_gv1_migration_buttons"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:constraint_referenced_ids="message_request_gv1_migration"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/message_request_busy_indicator"
style="?android:attr/progressBarStyle"

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/conversation_group_options__delivery"
android:id="@+id/menu_group_delivery"
android:icon="@drawable/ic_call_split_white_24dp"
app:iconTint="@color/signal_icon_tint_primary"
app:showAsAction="ifRoom" >
<menu>
<group android:id="@+id/distribution_group"
android:checkableBehavior="single">
<item android:id="@+id/menu_distribution_conversation" android:title="@string/conversation_group_options__conversation" android:checked="true" />
<item android:id="@+id/menu_distribution_broadcast" android:title="@string/conversation_group_options__broadcast" />
</group>
</menu>
</item>
</menu>

View File

@@ -1535,7 +1535,7 @@
<string name="MessageRequestBottomView_do_you_want_to_let_s_message_you_wont_receive_any_messages_until_you_unblock_them_SMS">Let %1$s message you? You won\'t receive any messages until you unblock them.</string>
<string name="MessageRequestBottomView_get_updates_and_news_from_s_you_wont_receive_any_updates_until_you_unblock_them">Get updates and news from %1$s? You won\'t receive any updates until you unblock them.</string>
<string name="MessageRequestBottomView_continue_your_conversation_with_this_group_and_share_your_name_and_photo">Continue your chat with this group and share your name and photo with its members?</string>
<string name="MessageRequestBottomView_upgrade_this_group_to_activate_new_features">Upgrade this group to activate new features like @mentions and admins. Members who have not shared their name or photo in this group will be invited to join.</string>
<string name="MessageRequestBottomView_upgrade_this_group_to_activate_new_features">This Legacy Group can no longer be used. Create a new group to activate new features like @mentions and admins.</string>
<string name="MessageRequestBottomView_this_legacy_group_can_no_longer_be_used">This Legacy Group can no longer be used because it is too large. The maximum group size is %1$d.</string>
<string name="MessageRequestBottomView_continue_your_conversation_with_s_and_share_your_name_and_photo">Continue your chat with %1$s and share your name and photo with them?</string>
<string name="MessageRequestBottomView_do_you_want_to_join_this_group_they_wont_know_youve_seen_their_messages_until_you_accept">Join this group and share your name and photo with its members? They won\'t know you\'ve seen their messages until you accept.</string>