Enforce a local GV2 capacity limit driven by a feature flag.

master
Alan Evans 2020-05-29 12:20:07 -03:00 committed by Greyson Parrelli
parent cfcd451db7
commit 9da309ca48
8 changed files with 129 additions and 18 deletions

View File

@ -29,6 +29,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.CycleInterpolator;
import android.widget.Button;
import android.widget.HorizontalScrollView;
import android.widget.TextView;
@ -67,7 +68,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -81,6 +81,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* Fragment for selecting a one or more contacts from a list.
@ -94,13 +95,16 @@ public final class ContactSelectionListFragment extends Fragment
@SuppressWarnings("unused")
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
private static final int CHIP_GROUP_EMPTY_COUNT = 1;
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 1;
private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150;
public static final String DISPLAY_MODE = "display_mode";
public static final String MULTI_SELECT = "multi_select";
public static final String REFRESHABLE = "refreshable";
public static final String RECENTS = "recents";
public static final int NO_LIMIT = Integer.MAX_VALUE;
public static final String DISPLAY_MODE = "display_mode";
public static final String MULTI_SELECT = "multi_select";
public static final String REFRESHABLE = "refreshable";
public static final String RECENTS = "recents";
public static final String SELECTION_LIMIT = "selection_limit";
private ConstraintLayout constraintLayout;
private TextView emptyText;
@ -116,12 +120,14 @@ public final class ContactSelectionListFragment extends Fragment
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private ChipGroup chipGroup;
private HorizontalScrollView chipGroupScrollContainer;
private TextView groupLimit;
@Nullable private FixedViewsAdapter headerAdapter;
@Nullable private FixedViewsAdapter footerAdapter;
@Nullable private ListCallback listCallback;
@Nullable private ScrollCallback scrollCallback;
private GlideRequests glideRequests;
private int selectionLimit;
@Override
public void onAttach(@NonNull Context context) {
@ -185,15 +191,29 @@ public final class ContactSelectionListFragment extends Fragment
showContactsProgress = view.findViewById(R.id.progress);
chipGroup = view.findViewById(R.id.chipGroup);
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
groupLimit = view.findViewById(R.id.group_limit);
constraintLayout = view.findViewById(R.id.container);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
selectionLimit = requireActivity().getIntent().getIntExtra(SELECTION_LIMIT, NO_LIMIT);
updateGroupLimit(getChipCount());
return view;
}
private void updateGroupLimit(int childCount) {
if (selectionLimit != NO_LIMIT) {
groupLimit.setText(String.format(Locale.getDefault(), "%d/%d", childCount, selectionLimit));
groupLimit.setVisibility(View.VISIBLE);
} else {
groupLimit.setVisibility(View.GONE);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
@ -408,6 +428,12 @@ public final class ContactSelectionListFragment extends Fragment
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
if (!isMulti() || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
if (selectionLimitReached()) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show();
groupLimit.animate().scaleX(1.3f).scaleY(1.3f).setInterpolator(new CycleInterpolator(0.5f)).start();
return;
}
if (contact.isUsernameType()) {
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
@ -447,6 +473,10 @@ public final class ContactSelectionListFragment extends Fragment
}}
}
private boolean selectionLimitReached() {
return getChipCount() >= selectionLimit;
}
private void markContactSelected(@NonNull SelectedContact selectedContact, @NonNull ContactSelectionListItem listItem) {
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
listItem.setChecked(true);
@ -469,7 +499,9 @@ public final class ContactSelectionListFragment extends Fragment
}
}
if (chipGroup.getChildCount() == CHIP_GROUP_EMPTY_COUNT) {
updateGroupLimit(getChipCount());
if (getChipCount() == 0) {
setChipGroupVisibility(ConstraintSet.GONE);
}
}
@ -477,7 +509,7 @@ public final class ContactSelectionListFragment extends Fragment
private void addChipForContact(@NonNull ContactSelectionListItem contact, @NonNull SelectedContact selectedContact) {
final ContactChip chip = new ContactChip(requireContext());
if (chipGroup.getChildCount() == CHIP_GROUP_EMPTY_COUNT) {
if (getChipCount() == 0) {
setChipGroupVisibility(ConstraintSet.VISIBLE);
}
@ -503,12 +535,23 @@ public final class ContactSelectionListFragment extends Fragment
LiveRecipient recipient = contact.getRecipient();
if (recipient != null) {
chip.setAvatar(glideRequests, recipient.get(), () -> chipGroup.addView(chip));
chip.setAvatar(glideRequests, recipient.get(), () -> addChip(chip));
} else {
chipGroup.addView(chip);
addChip(chip);
}
}
private void addChip(@NonNull ContactChip chip) {
chipGroup.addView(chip);
updateGroupLimit(getChipCount());
}
private int getChipCount() {
int count = chipGroup.getChildCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
if (count < 0) throw new AssertionError();
return count;
}
private void registerChipRecipientObserver(@NonNull ContactChip chip, @Nullable LiveRecipient recipient) {
if (recipient != null) {
recipient.observe(getViewLifecycleOwner(), resolved -> {

View File

@ -44,6 +44,7 @@ public class CreateGroupActivity extends ContactSelectionActivity {
: ContactsCursorLoader.DisplayMode.FLAG_PUSH;
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMIT, FeatureFlags.gv2GroupCapacity() - 1);
return intent;
}

View File

@ -23,14 +23,12 @@ import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProviders;
import org.thoughtcrime.securesms.AvatarPreviewActivity;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.MuteDialog;
import org.thoughtcrime.securesms.PushContactSelectionActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.groups.GroupId;
@ -225,11 +223,7 @@ public class ManageGroupFragment extends Fragment {
disappearingMessagesRow.setOnClickListener(v -> viewModel.handleExpirationSelection());
blockGroup.setOnClickListener(v -> viewModel.blockAndLeave(requireActivity()));
addMembers.setOnClickListener(v -> {
Intent intent = new Intent(requireActivity(), PushContactSelectionActivity.class);
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_PUSH);
startActivityForResult(intent, PICK_CONTACT);
});
addMembers.setOnClickListener(v -> viewModel.onAddMembersClick(this, PICK_CONTACT));
viewModel.getMembershipRights().observe(getViewLifecycleOwner(), r -> {
if (r != null) {

View File

@ -6,7 +6,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.groups.GroupAccessControl;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
@ -21,6 +24,7 @@ import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
@ -47,6 +51,18 @@ final class ManageGroupRepository {
SignalExecutors.BOUNDED.execute(() -> onGroupStateLoaded.accept(getGroupState()));
}
void getGroupCapacity(@NonNull Consumer<GroupCapacityResult> onGroupCapacityLoaded) {
SimpleTask.run(SignalExecutors.BOUNDED, () -> {
GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get();
if (groupRecord.isV2Group()) {
DecryptedGroup decryptedGroup = groupRecord.requireV2GroupProperties().getDecryptedGroup();
return new GroupCapacityResult(decryptedGroup.getMembersCount(), decryptedGroup.getPendingMembersCount(), FeatureFlags.gv2GroupCapacity());
} else {
return new GroupCapacityResult(groupRecord.getMembers().size(), 0, ContactSelectionListFragment.NO_LIMIT);
}
}, onGroupCapacityLoaded::accept);
}
@WorkerThread
private GroupStateResult getGroupState() {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
@ -152,4 +168,20 @@ final class ManageGroupRepository {
}
}
static final class GroupCapacityResult {
private final int fullMembers;
private final int pendingMembers;
private final int totalCapacity;
GroupCapacityResult(int fullMembers, int pendingMembers, int totalCapacity) {
this.fullMembers = fullMembers;
this.pendingMembers = pendingMembers;
this.totalCapacity = totalCapacity;
}
public int getRemainingCapacity() {
return totalCapacity - fullMembers - pendingMembers;
}
}
}

View File

@ -1,11 +1,13 @@
package org.thoughtcrime.securesms.groups.ui.managegroup;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@ -14,7 +16,11 @@ import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.BlockUnblockDialog;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.ExpirationDialog;
import org.thoughtcrime.securesms.PushContactSelectionActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
@ -211,6 +217,20 @@ public class ManageGroupViewModel extends ViewModel {
Util.runOnMain(() -> Toast.makeText(context, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show());
}
public void onAddMembersClick(@NonNull Fragment fragment, int resultCode) {
manageGroupRepository.getGroupCapacity(capacity -> {
int remainingCapacity = capacity.getRemainingCapacity();
if (remainingCapacity <= 0) {
Toast.makeText(fragment.requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show();
} else {
Intent intent = new Intent(fragment.requireActivity(), PushContactSelectionActivity.class);
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_PUSH);
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMIT, remainingCapacity);
fragment.startActivityForResult(intent, resultCode);
}
});
}
static final class GroupViewState {
private final long threadId;
@NonNull private final Recipient groupRecipient;

View File

@ -65,6 +65,7 @@ public final class FeatureFlags {
private static final String VERSIONED_PROFILES = "android.versionedProfiles";
private static final String GROUPS_V2 = "android.groupsv2";
private static final String GROUPS_V2_CREATE = "android.groupsv2.create";
private static final String GROUPS_V2_CAPACITY = "android.groupsv2.capacity";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@ -87,6 +88,7 @@ public final class FeatureFlags {
VERSIONED_PROFILES,
GROUPS_V2,
GROUPS_V2_CREATE,
GROUPS_V2_CAPACITY,
NEW_GROUP_UI
);
@ -283,6 +285,13 @@ public final class FeatureFlags {
return groupsV2() && getBoolean(GROUPS_V2_CREATE, false);
}
/**
* Maximum number of members allowed in a group.
*/
public static int gv2GroupCapacity() {
return getInteger(GROUPS_V2_CAPACITY, 100);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View File

@ -110,6 +110,17 @@
</LinearLayout>
<TextView
android:id="@+id/group_limit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@+id/chipGroupScrollContainer"
app:layout_constraintTop_toTopOf="parent"
tools:text="999/999"
tools:visibility="visible" />
<HorizontalScrollView
android:id="@+id/chipGroupScrollContainer"
android:layout_width="match_parent"
@ -122,7 +133,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@id/group_limit"
tools:visibility="visible">
<com.google.android.material.chip.ChipGroup

View File

@ -1372,6 +1372,7 @@
<string name="ContactSelectionListFragment_username_not_found">Username not found</string>
<string name="ContactSelectionListFragment_s_is_not_a_signal_user">"%1$s" is not a Signal user. Please check the username and try again.</string>
<string name="ContactSelectionListFragment_okay">Okay</string>
<string name="ContactSelectionListFragment_the_group_is_full">The group is full</string>
<!-- blocked_contacts_fragment -->
<string name="blocked_contacts_fragment__no_blocked_contacts">No blocked contacts</string>