diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index a3a330a659..267b14003b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -77,8 +77,44 @@ public final class GroupManager { } @WorkerThread - public static boolean leaveGroup(@NonNull Context context, @NonNull GroupId.Push groupId) { - return GroupManagerV1.leaveGroup(context, groupId.requireV1()); + public static void leaveGroup(@NonNull Context context, @NonNull GroupId.Push groupId) + throws GroupChangeBusyException, GroupChangeFailedException, IOException + { + if (groupId.isV2()) { + try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) { + edit.leaveGroup(); + Log.i(TAG, "Left group " + groupId); + } catch (GroupInsufficientRightsException e) { + Log.w(TAG, "Unexpected prevention from leaving " + groupId + " due to rights", e); + throw new GroupChangeFailedException(e); + } catch (GroupNotAMemberException e) { + Log.w(TAG, "Already left group " + groupId, e); + } + } else { + if (!GroupManagerV1.leaveGroup(context, groupId.requireV1())) { + Log.w(TAG, "GV1 group leave failed" + groupId); + throw new GroupChangeFailedException(); + } + } + } + + @WorkerThread + public static boolean silentLeaveGroup(@NonNull Context context, @NonNull GroupId.Push groupId) { + if (groupId.isV2()) { + throw new AssertionError("NYI"); // TODO [Alan] GV2 support silent leave for block and leave operations on GV2 + } else { + return GroupManagerV1.silentLeaveGroup(context, groupId.requireV1()); + } + } + + @WorkerThread + public static void ejectFromGroup(@NonNull Context context, @NonNull GroupId.V2 groupId, @NonNull Recipient recipient) + throws GroupChangeBusyException, GroupChangeFailedException, GroupInsufficientRightsException, GroupNotAMemberException, IOException + { + try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) { + edit.ejectMember(recipient.getId()); + Log.i(TAG, "Member removed from group " + groupId); + } } @WorkerThread diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java index 8764289065..b20ac86439 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java @@ -171,10 +171,35 @@ final class GroupManagerV1 { groupDatabase.remove(groupId, Recipient.self().getId()); return true; } else { + Log.i(TAG, "Group was already inactive. Skipping."); return false; } } + @WorkerThread + static boolean silentLeaveGroup(@NonNull Context context, @NonNull GroupId.V1 groupId) { + if (DatabaseFactory.getGroupDatabase(context).isActive(groupId)) { + Recipient groupRecipient = Recipient.externalGroup(context, groupId); + long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); + Optional leaveMessage = GroupUtil.createGroupLeaveMessage(context, groupRecipient); + + if (threadId != -1 && leaveMessage.isPresent()) { + ApplicationDependencies.getJobManager().add(LeaveGroupJob.create(groupRecipient)); + + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + groupDatabase.setActive(groupId, false); + groupDatabase.remove(groupId, Recipient.self().getId()); + return true; + } else { + Log.w(TAG, "Failed to leave group."); + return false; + } + } else { + Log.i(TAG, "Group was already inactive. Skipping."); + return true; + } + } + @WorkerThread static void updateGroupTimer(@NonNull Context context, @NonNull GroupId.V1 groupId, int expirationTime) { RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index 179bcd5f1c..c3e7268d1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -245,6 +245,27 @@ final class GroupManagerV2 { return commitChangeWithConflictResolution(groupOperations.createChangeMemberRole(recipient.getUuid().get(), admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT)); } + @WorkerThread + @NonNull GroupManager.GroupActionResult leaveGroup() + throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException + { + return ejectMember(Recipient.self().getId()); + } + + @WorkerThread + @NonNull GroupManager.GroupActionResult ejectMember(@NonNull RecipientId recipientId) + throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException + { + Recipient recipient = Recipient.resolved(recipientId); + GroupManager.GroupActionResult result = commitChangeWithConflictResolution(groupOperations.createRemoveMembersChange(Collections.singleton(recipient.getUuid().get()))); + + if (recipient.isLocalNumber()) { + groupDatabase.setActive(groupId, false); + } + + return result; + } + @WorkerThread @Nullable GroupManager.GroupActionResult updateSelfProfileKeyInGroup() throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupChangeErrorCallback.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupChangeErrorCallback.java new file mode 100644 index 0000000000..e9dab3dc11 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupChangeErrorCallback.java @@ -0,0 +1,7 @@ +package org.thoughtcrime.securesms.groups.ui; + +import androidx.annotation.NonNull; + +public interface GroupChangeErrorCallback { + void onError(@NonNull GroupChangeFailureReason failureReason); +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupChangeFailureReason.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupChangeFailureReason.java new file mode 100644 index 0000000000..895f003462 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupChangeFailureReason.java @@ -0,0 +1,8 @@ +package org.thoughtcrime.securesms.groups.ui; + +public enum GroupChangeFailureReason { + NO_RIGHTS, + NOT_CAPABLE, + NOT_A_MEMBER, + OTHER +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupErrors.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupErrors.java new file mode 100644 index 0000000000..ff3ff46d67 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupErrors.java @@ -0,0 +1,20 @@ +package org.thoughtcrime.securesms.groups.ui; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import org.thoughtcrime.securesms.R; + +public final class GroupErrors { + private GroupErrors() { + } + + public static @StringRes int getUserDisplayMessage(@NonNull GroupChangeFailureReason failureReason) { + switch (failureReason) { + case NO_RIGHTS : return R.string.ManageGroupActivity_you_dont_have_the_rights_to_do_this; + case NOT_CAPABLE : return R.string.ManageGroupActivity_not_capable; + case NOT_A_MEMBER: return R.string.ManageGroupActivity_youre_not_a_member_of_the_group; + default : return R.string.ManageGroupActivity_failed_to_update_the_group; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/LeaveGroupDialog.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/LeaveGroupDialog.java index 027c695e54..e37e603398 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/LeaveGroupDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/LeaveGroupDialog.java @@ -9,12 +9,19 @@ import androidx.appcompat.app.AlertDialog; import androidx.lifecycle.Lifecycle; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.groups.GroupChangeBusyException; +import org.thoughtcrime.securesms.groups.GroupChangeFailedException; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupManager; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.util.concurrent.SimpleTask; +import java.io.IOException; + public final class LeaveGroupDialog { + private static final String TAG = Log.tag(LeaveGroupDialog.class); + private LeaveGroupDialog() { } @@ -31,8 +38,16 @@ public final class LeaveGroupDialog { .setPositiveButton(R.string.yes, (dialog, which) -> SimpleTask.run( lifecycle, - () -> GroupManager.leaveGroup(context, groupId), - (success) -> { + () -> { + try { + GroupManager.leaveGroup(context, groupId); + return true; + } catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) { + Log.w(TAG, e); + return false; + } + }, + (success) -> { if (success) { if (onSuccess != null) onSuccess.run(); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java index 10394c00e3..ce3cc08a82 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java @@ -3,11 +3,9 @@ package org.thoughtcrime.securesms.groups.ui.managegroup; import android.content.Context; import androidx.annotation.NonNull; -import androidx.annotation.StringRes; import androidx.annotation.WorkerThread; import androidx.core.util.Consumer; -import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.groups.GroupAccessControl; @@ -18,6 +16,8 @@ import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException; import org.thoughtcrime.securesms.groups.GroupManager; import org.thoughtcrime.securesms.groups.GroupNotAMemberException; import org.thoughtcrime.securesms.groups.MembershipNotSuitableForV2Exception; +import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback; +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; @@ -26,7 +26,6 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import java.io.IOException; import java.util.List; -import java.util.concurrent.ExecutorService; final class ManageGroupRepository { @@ -57,47 +56,47 @@ final class ManageGroupRepository { return new GroupStateResult(threadId, groupRecipient); } - void setExpiration(int newExpirationTime, @NonNull Error error) { + void setExpiration(int newExpirationTime, @NonNull GroupChangeErrorCallback error) { SignalExecutors.UNBOUNDED.execute(() -> { try { GroupManager.updateGroupTimer(context, groupId.requirePush(), newExpirationTime); } catch (GroupInsufficientRightsException e) { Log.w(TAG, e); - error.onError(FailureReason.NO_RIGHTS); + error.onError(GroupChangeFailureReason.NO_RIGHTS); } catch (GroupNotAMemberException e) { Log.w(TAG, e); - error.onError(FailureReason.NOT_A_MEMBER); + error.onError(GroupChangeFailureReason.NOT_A_MEMBER); } catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) { Log.w(TAG, e); - error.onError(FailureReason.OTHER); + error.onError(GroupChangeFailureReason.OTHER); } }); } - void applyMembershipRightsChange(@NonNull GroupAccessControl newRights, @NonNull Error error) { + void applyMembershipRightsChange(@NonNull GroupAccessControl newRights, @NonNull GroupChangeErrorCallback error) { SignalExecutors.UNBOUNDED.execute(() -> { try { GroupManager.applyMembershipAdditionRightsChange(context, groupId.requireV2(), newRights); } catch (GroupInsufficientRightsException | GroupNotAMemberException e) { Log.w(TAG, e); - error.onError(FailureReason.NO_RIGHTS); + error.onError(GroupChangeFailureReason.NO_RIGHTS); } catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) { Log.w(TAG, e); - error.onError(FailureReason.OTHER); + error.onError(GroupChangeFailureReason.OTHER); } }); } - void applyAttributesRightsChange(@NonNull GroupAccessControl newRights, @NonNull Error error) { + void applyAttributesRightsChange(@NonNull GroupAccessControl newRights, @NonNull GroupChangeErrorCallback error) { SignalExecutors.UNBOUNDED.execute(() -> { try { GroupManager.applyAttributesRightsChange(context, groupId.requireV2(), newRights); } catch (GroupInsufficientRightsException | GroupNotAMemberException e) { Log.w(TAG, e); - error.onError(FailureReason.NO_RIGHTS); + error.onError(GroupChangeFailureReason.NO_RIGHTS); } catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) { Log.w(TAG, e); - error.onError(FailureReason.OTHER); + error.onError(GroupChangeFailureReason.OTHER); } }); } @@ -115,19 +114,19 @@ final class ManageGroupRepository { }); } - void addMembers(@NonNull List selected, @NonNull Error error) { + void addMembers(@NonNull List selected, @NonNull GroupChangeErrorCallback error) { SignalExecutors.UNBOUNDED.execute(() -> { try { GroupManager.addMembers(context, groupId, selected); } catch (GroupInsufficientRightsException | GroupNotAMemberException e) { Log.w(TAG, e); - error.onError(FailureReason.NO_RIGHTS); + error.onError(GroupChangeFailureReason.NO_RIGHTS); } catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) { Log.w(TAG, e); - error.onError(FailureReason.OTHER); + error.onError(GroupChangeFailureReason.OTHER); } catch (MembershipNotSuitableForV2Exception e) { Log.w(TAG, e); - error.onError(FailureReason.NOT_CAPABLE); + error.onError(GroupChangeFailureReason.NOT_CAPABLE); } }); } @@ -153,24 +152,4 @@ final class ManageGroupRepository { } } - public enum FailureReason { - NO_RIGHTS(R.string.ManageGroupActivity_you_dont_have_the_rights_to_do_this), - NOT_CAPABLE(R.string.ManageGroupActivity_not_capable), - NOT_A_MEMBER(R.string.ManageGroupActivity_youre_not_a_member_of_the_group), - OTHER(R.string.ManageGroupActivity_failed_to_update_the_group); - - private final @StringRes int toastMessage; - - FailureReason(@StringRes int toastMessage) { - this.toastMessage = toastMessage; - } - - public @StringRes int getToastMessage() { - return toastMessage; - } - } - - public interface Error { - void onError(@NonNull FailureReason failureReason); - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java index 20b1ed88f9..2fb1d7e5dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java @@ -21,6 +21,8 @@ import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader; import org.thoughtcrime.securesms.groups.GroupAccessControl; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.LiveGroup; +import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; +import org.thoughtcrime.securesms.groups.ui.GroupErrors; import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -205,8 +207,8 @@ public class ManageGroupViewModel extends ViewModel { } @WorkerThread - private void showErrorToast(@NonNull ManageGroupRepository.FailureReason e) { - Util.runOnMain(() -> Toast.makeText(context, e.getToastMessage(), Toast.LENGTH_LONG).show()); + private void showErrorToast(@NonNull GroupChangeFailureReason e) { + Util.runOnMain(() -> Toast.makeText(context, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show()); } static final class GroupViewState { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java index b20c9c34ae..686accaaac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java @@ -125,7 +125,7 @@ public class LeaveGroupJob extends BaseJob { } private static @NonNull List deliver(@NonNull Context context, - @NonNull GroupId groupId, + @NonNull GroupId.Push groupId, @NonNull String name, @NonNull List members, @NonNull List destinations) diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java index 678ad88887..daddff547d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java @@ -12,22 +12,18 @@ import com.annimon.stream.Stream; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.groups.GroupManager; import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; -import org.thoughtcrime.securesms.jobs.LeaveGroupJob; import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob; import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.FeatureFlags; -import org.thoughtcrime.securesms.util.GroupUtil; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -116,23 +112,9 @@ public class RecipientUtil { throw new AssertionError("Not a group!"); } - if (DatabaseFactory.getGroupDatabase(context).isActive(resolved.requireGroupId())) { - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(resolved); - Optional leaveMessage = GroupUtil.createGroupLeaveMessage(context, resolved); - - if (threadId != -1 && leaveMessage.isPresent()) { - ApplicationDependencies.getJobManager().add(LeaveGroupJob.create(recipient)); - - GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - GroupId groupId = resolved.requireGroupId(); - groupDatabase.setActive(groupId, false); - groupDatabase.remove(groupId, Recipient.self().getId()); - } else { - Log.w(TAG, "Failed to leave group."); - Toast.makeText(context, R.string.RecipientPreferenceActivity_error_leaving_group, Toast.LENGTH_LONG).show(); - } - } else { - Log.i(TAG, "Group was already inactive. Skipping."); + if (!GroupManager.silentLeaveGroup(context, resolved.requireGroupId().requirePush())) { + Log.w(TAG, "Failed to leave group."); + Toast.makeText(context, R.string.RecipientPreferenceActivity_error_leaving_group, Toast.LENGTH_LONG).show(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java index e70e3f8654..8aa8c15760 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java @@ -6,6 +6,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; +import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.NonNull; @@ -40,6 +41,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF private Button makeGroupAdminButton; private Button removeAdminButton; private Button removeFromGroupButton; + private ProgressBar adminActionBusy; public static BottomSheetDialogFragment create(@NonNull RecipientId recipientId, @Nullable GroupId groupId) @@ -80,6 +82,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF makeGroupAdminButton = view.findViewById(R.id.make_group_admin_button); removeAdminButton = view.findViewById(R.id.remove_group_admin_button); removeFromGroupButton = view.findViewById(R.id.remove_from_group_button); + adminActionBusy = view.findViewById(R.id.admin_action_busy); return view; } @@ -150,8 +153,17 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF blockButton.setOnClickListener(view -> viewModel.onBlockClicked(requireActivity())); unblockButton.setOnClickListener(view -> viewModel.onUnblockClicked(requireActivity())); - makeGroupAdminButton.setOnClickListener(view -> viewModel.onMakeGroupAdminClicked()); - removeAdminButton.setOnClickListener(view -> viewModel.onRemoveGroupAdminClicked()); - removeFromGroupButton.setOnClickListener(view -> viewModel.onRemoveFromGroupClicked()); + makeGroupAdminButton.setOnClickListener(view -> viewModel.onMakeGroupAdminClicked(requireActivity())); + removeAdminButton.setOnClickListener(view -> viewModel.onRemoveGroupAdminClicked(requireActivity())); + + removeFromGroupButton.setOnClickListener(view -> viewModel.onRemoveFromGroupClicked(requireActivity(), this::dismiss)); + + viewModel.getAdminActionBusy().observe(getViewLifecycleOwner(), busy -> { + adminActionBusy.setVisibility(busy ? View.VISIBLE : View.GONE); + + makeGroupAdminButton.setEnabled(!busy); + removeAdminButton.setEnabled(!busy); + removeFromGroupButton.setEnabled(!busy); + }); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java index e156f9b448..b2e7e2c2cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java @@ -4,17 +4,31 @@ import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.Consumer; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.IdentityDatabase; +import org.thoughtcrime.securesms.groups.GroupChangeBusyException; +import org.thoughtcrime.securesms.groups.GroupChangeFailedException; import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException; +import org.thoughtcrime.securesms.groups.GroupManager; +import org.thoughtcrime.securesms.groups.GroupNotAMemberException; +import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback; +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.concurrent.SignalExecutors; import org.thoughtcrime.securesms.util.concurrent.SimpleTask; +import java.io.IOException; +import java.util.Objects; + final class RecipientDialogRepository { + private static final String TAG = Log.tag(RecipientDialogRepository.class); + @NonNull private final Context context; @NonNull private final RecipientId recipientId; @Nullable private final GroupId groupId; @@ -52,6 +66,48 @@ final class RecipientDialogRepository { recipientCallback::onRecipient); } + void getGroupName(@NonNull Consumer stringConsumer) { + SimpleTask.run(SignalExecutors.BOUNDED, + () -> DatabaseFactory.getGroupDatabase(context).requireGroup(Objects.requireNonNull(groupId)).getTitle(), + stringConsumer::accept); + } + + void removeMember(@NonNull Consumer onComplete, @NonNull GroupChangeErrorCallback error) { + SimpleTask.run(SignalExecutors.UNBOUNDED, + () -> { + try { + GroupManager.ejectFromGroup(context, Objects.requireNonNull(groupId).requireV2(), Recipient.resolved(recipientId)); + return true; + } catch (GroupInsufficientRightsException | GroupNotAMemberException e) { + Log.w(TAG, e); + error.onError(GroupChangeFailureReason.NO_RIGHTS); + } catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) { + Log.w(TAG, e); + error.onError(GroupChangeFailureReason.OTHER); + } + return false; + }, + onComplete::accept); + } + + void setMemberAdmin(boolean admin, @NonNull Consumer onComplete, @NonNull GroupChangeErrorCallback error) { + SimpleTask.run(SignalExecutors.UNBOUNDED, + () -> { + try { + GroupManager.setMemberAdmin(context, Objects.requireNonNull(groupId).requireV2(), recipientId, admin); + return true; + } catch (GroupInsufficientRightsException | GroupNotAMemberException e) { + Log.w(TAG, e); + error.onError(GroupChangeFailureReason.NO_RIGHTS); + } catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) { + Log.w(TAG, e); + error.onError(GroupChangeFailureReason.OTHER); + } + return false; + }, + onComplete::accept); + } + interface IdentityCallback { void remoteIdentity(@Nullable IdentityDatabase.IdentityRecord identityRecord); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java index 700143ec3c..538cfc6ac8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java @@ -2,9 +2,12 @@ package org.thoughtcrime.securesms.recipients.ui.bottomsheet; import android.app.Activity; import android.content.Context; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -12,17 +15,23 @@ import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import org.thoughtcrime.securesms.BlockUnblockDialog; +import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.RecipientPreferenceActivity; import org.thoughtcrime.securesms.VerifyIdentityActivity; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.LiveGroup; +import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; +import org.thoughtcrime.securesms.groups.ui.GroupErrors; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.util.CommunicationActions; +import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; +import java.util.Objects; + final class RecipientDialogViewModel extends ViewModel { private final Context context; @@ -30,6 +39,7 @@ final class RecipientDialogViewModel extends ViewModel { private final LiveData recipient; private final MutableLiveData identity; private final LiveData adminActionStatus; + private final MutableLiveData adminActionBusy; private RecipientDialogViewModel(@NonNull Context context, @NonNull RecipientDialogRepository recipientDialogRepository) @@ -37,6 +47,7 @@ final class RecipientDialogViewModel extends ViewModel { this.context = context; this.recipientDialogRepository = recipientDialogRepository; this.identity = new MutableLiveData<>(); + this.adminActionBusy = new MutableLiveData<>(false); boolean recipientIsSelf = recipientDialogRepository.getRecipientId().equals(Recipient.self().getId()); @@ -72,6 +83,10 @@ final class RecipientDialogViewModel extends ViewModel { return identity; } + LiveData getAdminActionBusy() { + return adminActionBusy; + } + void onMessageClicked(@NonNull Activity activity) { recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startConversation(activity, recipient, null)); } @@ -96,19 +111,64 @@ final class RecipientDialogViewModel extends ViewModel { activity.startActivity(RecipientPreferenceActivity.getLaunchIntent(activity, recipientDialogRepository.getRecipientId())); } - void onMakeGroupAdminClicked() { - // TODO GV2 - throw new AssertionError("NYI"); + void onMakeGroupAdminClicked(@NonNull Activity activity) { + new AlertDialog.Builder(activity) + .setMessage(context.getString(R.string.RecipientBottomSheet_s_will_be_able_to_edit_group, Objects.requireNonNull(recipient.getValue()).toShortString(context))) + .setPositiveButton(R.string.RecipientBottomSheet_make_group_admin, + (dialog, which) -> { + adminActionBusy.setValue(true); + recipientDialogRepository.setMemberAdmin(true, result -> { + adminActionBusy.setValue(false); + if (!result) { + Toast.makeText(activity, R.string.ManageGroupActivity_failed_to_update_the_group, Toast.LENGTH_SHORT).show(); + } + }, + this::showErrorToast); + }) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> {}) + .show(); } - void onRemoveGroupAdminClicked() { - // TODO GV2 - throw new AssertionError("NYI"); + void onRemoveGroupAdminClicked(@NonNull Activity activity) { + new AlertDialog.Builder(activity) + .setMessage(context.getString(R.string.RecipientBottomSheet_remove_s_as_group_admin, Objects.requireNonNull(recipient.getValue()).toShortString(context))) + .setPositiveButton(R.string.RecipientBottomSheet_remove_as_admin, + (dialog, which) -> { + adminActionBusy.setValue(true); + recipientDialogRepository.setMemberAdmin(false, result -> { + adminActionBusy.setValue(false); + if (!result) { + Toast.makeText(activity, R.string.ManageGroupActivity_failed_to_update_the_group, Toast.LENGTH_SHORT).show(); + } + }, + this::showErrorToast); + }) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> {}) + .show(); } - void onRemoveFromGroupClicked() { - // TODO GV2 - throw new AssertionError("NYI"); + void onRemoveFromGroupClicked(@NonNull Activity activity, @NonNull Runnable onSuccess) { + recipientDialogRepository.getGroupName(title -> + new AlertDialog.Builder(activity) + .setMessage(context.getString(R.string.RecipientBottomSheet_remove_s_from_s, Objects.requireNonNull(recipient.getValue()).toShortString(context), title)) + .setPositiveButton(R.string.RecipientBottomSheet_remove, + (dialog, which) -> { + adminActionBusy.setValue(true); + recipientDialogRepository.removeMember(result -> { + adminActionBusy.setValue(false); + if (result) { + onSuccess.run(); + } + }, + this::showErrorToast); + }) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> {}) + .show()); + } + + @WorkerThread + private void showErrorToast(@NonNull GroupChangeFailureReason e) { + Util.runOnMain(() -> Toast.makeText(context, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show()); } static class AdminActionStatus { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ece6053059..843dbe3e46 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2228,6 +2228,12 @@ Remove as admin Remove from group + Remove %1$s as group admin? + %1$s will be able to edit this group and its members + + Remove %1$s from "%2$s"? + Remove + Admin diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java index 2ebfd37da1..3a2b692b07 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java @@ -192,6 +192,17 @@ public final class GroupsV2Operations { return actions; } + public GroupChange.Actions.Builder createRemoveMembersChange(final Set membersToRemove) { + GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder(); + + for (UUID remove: membersToRemove) { + actions.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.newBuilder() + .setDeletedUserId(encryptUuid(remove))); + } + + return actions; + } + public GroupChange.Actions.Builder createModifyGroupTimerChange(int timerDurationSeconds) { return GroupChange.Actions .newBuilder()