mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Remove GV1 migration support.
This commit is contained in:
committed by
Greyson Parrelli
parent
213d996168
commit
34a228f85e
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package org.thoughtcrime.securesms.groups.ui.migration;
|
||||
|
||||
enum MigrationResult {
|
||||
SUCCESS, FAILURE_GENERAL, FAILURE_NETWORK
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user