Signal-Android/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java

247 lines
12 KiB
Java

package org.thoughtcrime.securesms.groups;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
import org.thoughtcrime.securesms.jobs.LeaveGroupJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
final class GroupManagerV1 {
private static final String TAG = Log.tag(GroupManagerV1.class);
static @NonNull GroupActionResult createGroup(@NonNull Context context,
@NonNull Set<RecipientId> memberIds,
@Nullable byte[] avatarBytes,
@Nullable String name,
boolean mms)
{
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
final SecureRandom secureRandom = new SecureRandom();
final GroupId groupId = mms ? GroupId.createMms(secureRandom) : GroupId.createV1(secureRandom);
final RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
final Recipient groupRecipient = Recipient.resolved(groupRecipientId);
memberIds.add(Recipient.self().getId());
if (groupId.isV1()) {
GroupId.V1 groupIdV1 = groupId.requireV1();
groupDatabase.create(groupIdV1, name, memberIds, null, null);
try {
AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null);
} catch (IOException e) {
Log.w(TAG, "Failed to save avatar!", e);
}
groupDatabase.onAvatarUpdated(groupIdV1, avatarBytes != null);
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true);
return sendGroupUpdate(context, groupIdV1, memberIds, name, avatarBytes, memberIds.size() - 1);
} else {
groupDatabase.create(groupId.requireMms(), memberIds);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
return new GroupActionResult(groupRecipient, threadId, memberIds.size() - 1, Collections.emptyList());
}
}
static GroupActionResult updateGroup(@NonNull Context context,
@NonNull GroupId groupId,
@NonNull Set<RecipientId> memberAddresses,
@Nullable byte[] avatarBytes,
@Nullable String name,
int newMemberCount)
{
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
final RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
memberAddresses.add(Recipient.self().getId());
groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses));
if (groupId.isPush()) {
GroupId.V1 groupIdV1 = groupId.requireV1();
groupDatabase.updateTitle(groupIdV1, name);
groupDatabase.onAvatarUpdated(groupIdV1, 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 sendGroupUpdate(context, groupIdV1, memberAddresses, name, avatarBytes, newMemberCount);
} else {
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
return new GroupActionResult(groupRecipient, threadId, newMemberCount, Collections.emptyList());
}
}
private static GroupActionResult sendGroupUpdate(@NonNull Context context,
@NonNull GroupId.V1 groupId,
@NonNull Set<RecipientId> members,
@Nullable String groupName,
@Nullable byte[] avatar,
int newMemberCount)
{
Attachment avatarAttachment = null;
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
List<GroupContext.Member> uuidMembers = new ArrayList<>(members.size());
List<String> e164Members = new ArrayList<>(members.size());
for (RecipientId member : members) {
Recipient recipient = Recipient.resolved(member);
if (recipient.hasE164()) {
e164Members.add(recipient.requireE164());
uuidMembers.add(GroupV1MessageProcessor.createMember(recipient.requireE164()));
}
}
GroupContext.Builder groupContextBuilder = GroupContext.newBuilder()
.setId(ByteString.copyFrom(groupId.getDecodedId()))
.setType(GroupContext.Type.UPDATE)
.addAllMembersE164(e164Members)
.addAllMembers(uuidMembers);
if (groupName != null) groupContextBuilder.setName(groupName);
GroupContext groupContext = groupContextBuilder.build();
if (avatar != null) {
Uri avatarUri = BlobProvider.getInstance().forData(avatar).createForSingleUseInMemory();
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, false, null, null, null, null, null);
}
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList());
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);
return new GroupActionResult(groupRecipient, threadId, newMemberCount, Collections.emptyList());
}
@WorkerThread
static boolean leaveGroup(@NonNull Context context, @NonNull GroupId.V1 groupId) {
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
Optional<OutgoingGroupUpdateMessage> leaveMessage = createGroupLeaveMessage(context, groupId, groupRecipient);
if (threadId != -1 && leaveMessage.isPresent()) {
try {
long id = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(leaveMessage.get(), threadId, false, null);
DatabaseFactory.getMmsDatabase(context).markAsSent(id, true);
} catch (MmsException e) {
Log.w(TAG, "Failed to insert leave message.", e);
}
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.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<OutgoingGroupUpdateMessage> leaveMessage = createGroupLeaveMessage(context, groupId, 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);
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
Recipient recipient = Recipient.externalGroup(context, groupId);
long threadId = threadDatabase.getThreadIdFor(recipient);
recipientDatabase.setExpireMessages(recipient.getId(), expirationTime);
OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(recipient, System.currentTimeMillis(), expirationTime * 1000L);
MessageSender.send(context, outgoingMessage, threadId, false, null);
}
@WorkerThread
private static Optional<OutgoingGroupUpdateMessage> createGroupLeaveMessage(@NonNull Context context,
@NonNull GroupId.V1 groupId,
@NonNull Recipient groupRecipient)
{
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
if (!groupDatabase.isActive(groupId)) {
Log.w(TAG, "Group has already been left.");
return Optional.absent();
}
GroupContext groupContext = GroupContext.newBuilder()
.setId(ByteString.copyFrom(groupId.getDecodedId()))
.setType(GroupContext.Type.QUIT)
.build();
return Optional.of(new OutgoingGroupUpdateMessage(groupRecipient,
groupContext,
null,
System.currentTimeMillis(),
0,
false,
null,
Collections.emptyList(),
Collections.emptyList()));
}
}