Fix race condition by tracking removed contacts explicitly

Instead of comparing current members at result time (which can change),
ContactMultiSelectionActivity now tracks preselected contacts and
explicitly returns which ones were removed. This eliminates the race
condition where member list changes could cause accidental removals.

Changes:
- ContactMultiSelectionActivity: Store preselected contacts, calculate and return removed contacts
- ProfileFragment: Use explicitly removed contacts instead of calculating from current member list

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-02 23:03:32 +00:00
parent 88d6992a9e
commit deb4e356bd
2 changed files with 28 additions and 11 deletions
@@ -23,7 +23,9 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Activity container for selecting a list of contacts.
@@ -34,6 +36,9 @@ import java.util.List;
public class ContactMultiSelectionActivity extends ContactSelectionActivity {
public static final String CONTACTS_EXTRA = "contacts_extra";
public static final String REMOVED_CONTACTS_EXTRA = "removed_contacts_extra";
private ArrayList<Integer> preselectedContacts;
@Override
protected void onCreate(Bundle icicle, boolean ready) {
@@ -44,6 +49,9 @@ public class ContactMultiSelectionActivity extends ContactSelectionActivity {
// it's a bit confusing having one "X" button on the left and one on the right -
// and the "clear search" button is not that important.
getToolbar().setUseClearButton(false);
// Store preselected contacts to track which ones were removed
preselectedContacts = getIntent().getIntegerArrayListExtra(ContactSelectionListFragment.PRESELECTED_CONTACTS);
}
@Override
@@ -72,6 +80,19 @@ public class ContactMultiSelectionActivity extends ContactSelectionActivity {
Intent resultIntent = getIntent();
List<Integer> selectedContacts = contactsFragment.getSelectedContacts();
resultIntent.putIntegerArrayListExtra(CONTACTS_EXTRA, new ArrayList<>(selectedContacts));
// Calculate which contacts were removed (preselected but not in final selection)
if (preselectedContacts != null) {
Set<Integer> selectedSet = new HashSet<>(selectedContacts);
ArrayList<Integer> removedContacts = new ArrayList<>();
for (Integer preselectedId : preselectedContacts) {
if (!selectedSet.contains(preselectedId)) {
removedContacts.add(preselectedId);
}
}
resultIntent.putIntegerArrayListExtra(REMOVED_CONTACTS_EXTRA, removedContacts);
}
setResult(RESULT_OK, resultIntent);
}
}
@@ -34,9 +34,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ProfileFragment extends Fragment
implements ProfileAdapter.ItemClickListener, DcEventCenter.DcEventDelegate {
@@ -312,11 +310,9 @@ public class ProfileFragment extends Fragment
super.onActivityResult(requestCode, resultCode, data);
if (requestCode==REQUEST_CODE_PICK_CONTACT && resultCode==Activity.RESULT_OK && data!=null) {
List<Integer> selected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA);
List<Integer> removed = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.REMOVED_CONTACTS_EXTRA);
if(selected == null) return;
Util.runOnAnyBackgroundThread(() -> {
// Get current members
int[] currentMembers = dcContext.getChatContacts(chatId);
// Add new members
for (Integer contactId : selected) {
if (contactId != null) {
@@ -324,12 +320,12 @@ public class ProfileFragment extends Fragment
}
}
// Remove members that were unchecked
// Use HashSet for O(1) lookup performance
Set<Integer> selectedSet = new HashSet<>(selected);
for (int currentMemberId : currentMembers) {
if (!selectedSet.contains(currentMemberId)) {
dcContext.removeContactFromChat(chatId, currentMemberId);
// Remove members that were explicitly unchecked
if (removed != null) {
for (Integer contactId : removed) {
if (contactId != null) {
dcContext.removeContactFromChat(chatId, contactId);
}
}
}
});