diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 38bd05924b..80f63538f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.tracing.Trace; import org.thoughtcrime.securesms.util.CursorUtil; import org.thoughtcrime.securesms.util.SqlUtil; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -38,7 +39,6 @@ import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.Closeable; import java.security.SecureRandom; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -254,7 +254,7 @@ public final class GroupDatabase extends Database { .requireMms(); } else { GroupId.Mms groupId = GroupId.createMms(new SecureRandom()); - create(groupId, members); + create(groupId, null, members); return groupId; } } finally { @@ -364,9 +364,10 @@ public final class GroupDatabase extends Database { } public void create(@NonNull GroupId.Mms groupId, + @Nullable String title, @NonNull Collection members) { - create(groupId, null, members, null, null, null, null); + create(groupId, Util.isEmpty(title) ? null : title, members, null, null, null, null); } public GroupId.V2 create(@NonNull GroupMasterKey groupMasterKey, @@ -575,6 +576,18 @@ public final class GroupDatabase extends Database { } public void updateTitle(@NonNull GroupId.V1 groupId, String title) { + updateTitle((GroupId) groupId, title); + } + + public void updateTitle(@NonNull GroupId.Mms groupId, @Nullable String title) { + updateTitle((GroupId) groupId, Util.isEmpty(title) ? null : title); + } + + private void updateTitle(@NonNull GroupId groupId, String title) { + if (!groupId.isV1() && !groupId.isMms()) { + throw new AssertionError(); + } + ContentValues contentValues = new ContentValues(); contentValues.put(TITLE, title); databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", @@ -587,7 +600,7 @@ public final class GroupDatabase extends Database { /** * Used to bust the Glide cache when an avatar changes. */ - public void onAvatarUpdated(@NonNull GroupId.Push groupId, boolean hasAvatar) { + public void onAvatarUpdated(@NonNull GroupId groupId, boolean hasAvatar) { ContentValues contentValues = new ContentValues(1); contentValues.put(AVATAR_ID, hasAvatar ? Math.abs(new SecureRandom().nextLong()) : 0); @@ -962,7 +975,7 @@ public final class GroupDatabase extends Database { } return GroupAccessControl.ONLY_ADMINS; } else { - return id.isV1() ? GroupAccessControl.ALL_MEMBERS : GroupAccessControl.ONLY_ADMINS; + return GroupAccessControl.ALL_MEMBERS; } } 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 f5e29aef03..d9550bf7d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -73,13 +73,15 @@ public final class GroupManager { try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) { return edit.updateGroupTitleAndAvatar(nameChanged ? name : null, avatar, avatarChanged); } - } else { + } else if (groupId.isV1()) { List members = DatabaseFactory.getGroupDatabase(context) .getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); Set recipientIds = getMemberIds(new HashSet<>(members)); return GroupManagerV1.updateGroup(context, groupId.requireV1(), recipientIds, avatar, name, 0); + } else { + return GroupManagerV1.updateGroup(context, groupId.requireMms(), avatar, name); } } 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 0c68dfc5cc..85f7440f75 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java @@ -74,7 +74,15 @@ final class GroupManagerV1 { DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true); return sendGroupUpdate(context, groupIdV1, memberIds, name, avatarBytes, memberIds.size() - 1); } else { - groupDatabase.create(groupId.requireMms(), memberIds); + groupDatabase.create(groupId.requireMms(), name, memberIds); + + try { + AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null); + } catch (IOException e) { + Log.w(TAG, "Failed to save avatar!", e); + } + groupDatabase.onAvatarUpdated(groupId, avatarBytes != null); + long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); return new GroupActionResult(groupRecipient, threadId, memberIds.size() - 1, Collections.emptyList()); } @@ -112,6 +120,28 @@ final class GroupManagerV1 { } } + static GroupActionResult updateGroup(@NonNull Context context, + @NonNull GroupId.Mms groupId, + @Nullable byte[] avatarBytes, + @Nullable String name) + { + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId); + Recipient groupRecipient = Recipient.resolved(groupRecipientId); + long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); + + groupDatabase.updateTitle(groupId, name); + groupDatabase.onAvatarUpdated(groupId, avatarBytes != null); + + try { + AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null); + } catch (IOException e) { + Log.w(TAG, "Failed to save avatar!", e); + } + + return new GroupActionResult(groupRecipient, threadId, 0, Collections.emptyList()); + } + private static GroupActionResult sendGroupUpdate(@NonNull Context context, @NonNull GroupId.V1 groupId, @NonNull Set members, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java index e7aef1539b..544796573b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java @@ -116,8 +116,7 @@ public class AddGroupDetailsFragment extends LoggingFragment { viewModel.getCanSubmitForm().observe(getViewLifecycleOwner(), isFormValid -> setCreateEnabled(isFormValid, true)); viewModel.getIsMms().observe(getViewLifecycleOwner(), isMms -> { mmsWarning.setVisibility(isMms ? View.VISIBLE : View.GONE); - name.setVisibility(isMms ? View.GONE : View.VISIBLE); - avatar.setVisibility(isMms ? View.GONE : View.VISIBLE); + name.setHint(isMms ? R.string.AddGroupDetailsFragment__group_name_optional : R.string.AddGroupDetailsFragment__group_name_required); toolbar.setTitle(isMms ? R.string.AddGroupDetailsFragment__create_group : R.string.AddGroupDetailsFragment__name_this_group); }); viewModel.getNonGv2CapableMembers().observe(getViewLifecycleOwner(), nonGv2CapableMembers -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java index ed0b2e4616..9982816245 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java @@ -48,11 +48,11 @@ final class AddGroupDetailsRepository { }); } - void createPushGroup(@NonNull Set members, - @Nullable byte[] avatar, - @Nullable String name, - boolean mms, - Consumer resultConsumer) + void createGroup(@NonNull Set members, + @Nullable byte[] avatar, + @Nullable String name, + boolean mms, + Consumer resultConsumer) { SignalExecutors.BOUNDED.execute(() -> { Set recipients = new HashSet<>(Stream.of(members).map(Recipient::resolved).toList()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java index 86e5cfaa7e..45fd4b0c62 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java @@ -117,7 +117,7 @@ public final class AddGroupDetailsViewModel extends ViewModel { Set memberIds = Stream.of(members).map(member -> member.getMember().getId()).collect(Collectors.toSet()); byte[] avatarBytes = avatar.getValue(); boolean isGroupMms = isMms.getValue() == Boolean.TRUE; - String groupName = isGroupMms ? "" : name.getValue(); + String groupName = name.getValue(); if (!isGroupMms && TextUtils.isEmpty(groupName)) { groupCreateResult.postValue(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_INVALID_NAME)); @@ -129,11 +129,11 @@ public final class AddGroupDetailsViewModel extends ViewModel { return; } - repository.createPushGroup(memberIds, - avatarBytes, - groupName, - isGroupMms, - groupCreateResult::postValue); + repository.createGroup(memberIds, + avatarBytes, + groupName, + isGroupMms, + groupCreateResult::postValue); } private static @NonNull List filterDeletedMembers(@NonNull List members, @NonNull Set deleted) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java index 6bbc2f36e3..62d4c19b03 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java @@ -416,7 +416,7 @@ public class ManageGroupFragment extends LoggingFragment { public boolean onMenuItemSelected(@NonNull MenuItem item) { if (item.getItemId() == R.id.action_edit) { - startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), getGroupId().requirePush())); + startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), getGroupId())); return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditPushGroupProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditGroupProfileRepository.java similarity index 77% rename from app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditPushGroupProfileRepository.java rename to app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditGroupProfileRepository.java index 2e87f4a565..502c726fbe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditPushGroupProfileRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditGroupProfileRepository.java @@ -8,6 +8,7 @@ import androidx.annotation.WorkerThread; import androidx.core.util.Consumer; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.groups.GroupChangeException; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupManager; @@ -22,14 +23,14 @@ import org.whispersystems.libsignal.util.guava.Optional; import java.io.IOException; -class EditPushGroupProfileRepository implements EditProfileRepository { +class EditGroupProfileRepository implements EditProfileRepository { - private static final String TAG = Log.tag(EditPushGroupProfileRepository.class); + private static final String TAG = Log.tag(EditGroupProfileRepository.class); - private final Context context; - private final GroupId.Push groupId; + private final Context context; + private final GroupId groupId; - EditPushGroupProfileRepository(@NonNull Context context, @NonNull GroupId.Push groupId) { + EditGroupProfileRepository(@NonNull Context context, @NonNull GroupId groupId) { this.context = context.getApplicationContext(); this.groupId = groupId; } @@ -64,7 +65,18 @@ class EditPushGroupProfileRepository implements EditProfileRepository { @Override public void getCurrentName(@NonNull Consumer nameConsumer) { - SimpleTask.run(() -> Recipient.resolved(getRecipientId()).getName(context), nameConsumer::accept); + SimpleTask.run(() -> { + RecipientId recipientId = getRecipientId(); + Recipient recipient = Recipient.resolved(recipientId); + + return DatabaseFactory.getGroupDatabase(context) + .getGroup(recipientId) + .transform(groupRecord -> { + String title = groupRecord.getTitle(); + return title == null ? "" : title; + }) + .or(() -> recipient.getName(context)); + }, nameConsumer::accept); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java index 9b263bade6..123dbe6fdf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java @@ -44,7 +44,7 @@ public class EditProfileActivity extends BaseActivity implements EditProfileFrag return intent; } - public static @NonNull Intent getIntentForGroupProfile(@NonNull Context context, @NonNull GroupId.Push groupId) { + public static @NonNull Intent getIntentForGroupProfile(@NonNull Context context, @NonNull GroupId groupId) { Intent intent = new Intent(context, EditProfileActivity.class); intent.putExtra(EditProfileActivity.SHOW_TOOLBAR, true); intent.putExtra(EditProfileActivity.GROUP_ID, groupId.toString()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java index 6bd6ccf15e..7533c7f3c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java @@ -119,11 +119,10 @@ public class EditProfileFragment extends LoggingFragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - final GroupId groupId = GroupId.parseNullableOrThrow(requireArguments().getString(GROUP_ID, null)); - final GroupId.Push pushGroupId = groupId != null ? groupId.requirePush() : null; + GroupId groupId = GroupId.parseNullableOrThrow(requireArguments().getString(GROUP_ID, null)); - initializeResources(view, pushGroupId != null); - initializeViewModel(requireArguments().getBoolean(EXCLUDE_SYSTEM, false), pushGroupId, savedInstanceState != null); + initializeResources(view, groupId); + initializeViewModel(requireArguments().getBoolean(EXCLUDE_SYSTEM, false), groupId, savedInstanceState != null); initializeProfileAvatar(); initializeProfileName(); initializeUsername(); @@ -174,11 +173,11 @@ public class EditProfileFragment extends LoggingFragment { } } - private void initializeViewModel(boolean excludeSystem, @Nullable GroupId.Push groupId, boolean hasSavedInstanceState) { + private void initializeViewModel(boolean excludeSystem, @Nullable GroupId groupId, boolean hasSavedInstanceState) { EditProfileRepository repository; if (groupId != null) { - repository = new EditPushGroupProfileRepository(requireContext(), groupId); + repository = new EditGroupProfileRepository(requireContext(), groupId); } else { repository = new EditSelfProfileRepository(requireContext(), excludeSystem); } @@ -189,8 +188,9 @@ public class EditProfileFragment extends LoggingFragment { .get(EditProfileViewModel.class); } - private void initializeResources(@NonNull View view, boolean isEditingGroup) { - Bundle arguments = requireArguments(); + private void initializeResources(@NonNull View view, @Nullable GroupId groupId) { + Bundle arguments = requireArguments(); + boolean isEditingGroup = groupId != null; this.toolbar = view.findViewById(R.id.toolbar); this.title = view.findViewById(R.id.title); @@ -213,10 +213,13 @@ public class EditProfileFragment extends LoggingFragment { this.avatar.setOnClickListener(v -> startAvatarSelection()); - this.givenName .addTextChangedListener(new AfterTextChanged(s -> { - trimInPlace(s, isEditingGroup); - viewModel.setGivenName(s.toString()); - })); + this.givenName.addTextChangedListener(new AfterTextChanged(s -> { + trimInPlace(s, isEditingGroup); + viewModel.setGivenName(s.toString()); + })); + + view.findViewById(R.id.mms_group_hint) + .setVisibility(isEditingGroup && groupId.isMms() ? View.VISIBLE : View.GONE); if (isEditingGroup) { givenName.setHint(R.string.EditProfileFragment__group_name); diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java index 221da03ffa..ec0cb5fb78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.profiles.edit; -import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Consumer; @@ -30,13 +29,15 @@ class EditProfileViewModel extends ViewModel { private final MutableLiveData originalAvatar = new MutableLiveData<>(); private final MutableLiveData> internalUsername = new MutableLiveData<>(); private final MutableLiveData originalDisplayName = new MutableLiveData<>(); - private final LiveData isFormValid = Transformations.map(trimmedGivenName, s -> s.length() > 0); + private final LiveData isFormValid; private final EditProfileRepository repository; private final GroupId groupId; private EditProfileViewModel(@NonNull EditProfileRepository repository, boolean hasInstanceState, @Nullable GroupId groupId) { - this.repository = repository; - this.groupId = groupId; + this.repository = repository; + this.groupId = groupId; + this.isFormValid = groupId != null && groupId.isMms() ? LiveDataUtil.just(true) + : Transformations.map(trimmedGivenName, s -> s.length() > 0); if (!hasInstanceState) { if (groupId != null) { diff --git a/app/src/main/res/layout/add_group_details_fragment.xml b/app/src/main/res/layout/add_group_details_fragment.xml index 47ccc50aad..52bc13ed1f 100644 --- a/app/src/main/res/layout/add_group_details_fragment.xml +++ b/app/src/main/res/layout/add_group_details_fragment.xml @@ -40,23 +40,41 @@ app:layout_constraintStart_toEndOf="@id/group_avatar" app:layout_constraintTop_toTopOf="@id/group_avatar" /> - + tools:visibility="visible"> + + + + + + + + Create Members Group name (required) + Group name (optional) This field is required. Groups require at least two members. Group creation failed. Try again later. You\'ve selected a contact that doesn\'t support Signal groups, so this group will be MMS. + Custom MMS group names and photos will only be visible to you. Remove SMS contact Remove %1$s from this group? @@ -2073,6 +2075,7 @@ Next Username Create a username + Custom MMS group names and photos will only be visible to you. Edit group name and photo