mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 13:18:34 +00:00
Group Manager V2 operations.
This commit is contained in:
parent
48a693793f
commit
86f0456e8c
@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import org.signal.zkgroup.InvalidInputException;
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
import org.signal.zkgroup.profiles.ProfileKey;
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
@ -53,6 +54,18 @@ public final class ProfileKeyUtil {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static @Nullable ProfileKeyCredential profileKeyCredentialOrNull(@Nullable byte[] profileKeyCredential) {
|
||||||
|
if (profileKeyCredential != null) {
|
||||||
|
try {
|
||||||
|
return new ProfileKeyCredential(profileKeyCredential);
|
||||||
|
} catch (InvalidInputException e) {
|
||||||
|
Log.w(TAG, String.format(Locale.US, "Seen non-null profile key credential of wrong length %d", profileKeyCredential.length), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static @NonNull ProfileKey profileKeyOrThrow(@NonNull byte[] profileKey) {
|
public static @NonNull ProfileKey profileKeyOrThrow(@NonNull byte[] profileKey) {
|
||||||
try {
|
try {
|
||||||
return new ProfileKey(profileKey);
|
return new ProfileKey(profileKey);
|
||||||
@ -69,6 +82,10 @@ public final class ProfileKeyUtil {
|
|||||||
return Optional.of(profileKeyOrThrow(profileKey));
|
return Optional.of(profileKeyOrThrow(profileKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static @NonNull Optional<ProfileKeyCredential> profileKeyCredentialOptional(@Nullable byte[] profileKey) {
|
||||||
|
return Optional.fromNullable(profileKeyCredentialOrNull(profileKey));
|
||||||
|
}
|
||||||
|
|
||||||
public static @NonNull ProfileKey createNew() {
|
public static @NonNull ProfileKey createNew() {
|
||||||
try {
|
try {
|
||||||
return new ProfileKey(Util.getSecretBytes(32));
|
return new ProfileKey(Util.getSecretBytes(32));
|
||||||
|
@ -7,13 +7,14 @@ import androidx.annotation.NonNull;
|
|||||||
import org.thoughtcrime.securesms.BuildConfig;
|
import org.thoughtcrime.securesms.BuildConfig;
|
||||||
import org.thoughtcrime.securesms.IncomingMessageProcessor;
|
import org.thoughtcrime.securesms.IncomingMessageProcessor;
|
||||||
import org.thoughtcrime.securesms.gcm.MessageRetriever;
|
import org.thoughtcrime.securesms.gcm.MessageRetriever;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupsV2AuthorizationMemoryValueCache;
|
||||||
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
|
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
import org.thoughtcrime.securesms.keyvalue.KeyValueStore;
|
import org.thoughtcrime.securesms.keyvalue.KeyValueStore;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
||||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||||
import org.thoughtcrime.securesms.util.EarlyMessageCache;
|
import org.thoughtcrime.securesms.util.EarlyMessageCache;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
@ -24,8 +25,7 @@ import org.whispersystems.signalservice.api.KeyBackupService;
|
|||||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
import org.thoughtcrime.securesms.groups.GroupsV2Authorization;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Authorization;
|
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,7 +84,8 @@ public class ApplicationDependencies {
|
|||||||
assertInitialization();
|
assertInitialization();
|
||||||
|
|
||||||
if (groupsV2Authorization == null) {
|
if (groupsV2Authorization == null) {
|
||||||
groupsV2Authorization = getSignalServiceAccountManager().createGroupsV2Authorization(Recipient.self().getUuid().get());
|
GroupsV2Authorization.ValueCache authCache = new GroupsV2AuthorizationMemoryValueCache(SignalStore.groupsV2AuthorizationCache());
|
||||||
|
groupsV2Authorization = new GroupsV2Authorization(getSignalServiceAccountManager().getGroupsV2Api(), authCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
return groupsV2Authorization;
|
return groupsV2Authorization;
|
||||||
|
@ -11,9 +11,12 @@ import org.signal.zkgroup.VerificationFailedException;
|
|||||||
import org.signal.zkgroup.groups.UuidCiphertext;
|
import org.signal.zkgroup.groups.UuidCiphertext;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||||
|
|
||||||
@ -25,6 +28,8 @@ import java.util.Set;
|
|||||||
|
|
||||||
public final class GroupManager {
|
public final class GroupManager {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(GroupManager.class);
|
||||||
|
|
||||||
public static @NonNull GroupActionResult createGroup(@NonNull Context context,
|
public static @NonNull GroupActionResult createGroup(@NonNull Context context,
|
||||||
@NonNull Set<Recipient> members,
|
@NonNull Set<Recipient> members,
|
||||||
@Nullable Bitmap avatar,
|
@Nullable Bitmap avatar,
|
||||||
@ -87,13 +92,44 @@ public final class GroupManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
public static void setMemberAdmin(@NonNull Context context,
|
||||||
|
@NonNull GroupId.V2 groupId,
|
||||||
|
@NonNull RecipientId recipientId,
|
||||||
|
boolean admin)
|
||||||
|
throws GroupChangeBusyException, GroupChangeFailedException, GroupInsufficientRightsException, GroupNotAMemberException, IOException
|
||||||
|
{
|
||||||
|
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||||
|
editor.setMemberAdmin(recipientId, admin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
public static void updateSelfProfileKeyInGroup(@NonNull Context context, @NonNull GroupId.V2 groupId)
|
||||||
|
throws IOException, GroupChangeBusyException, GroupInsufficientRightsException, GroupNotAMemberException, GroupChangeFailedException
|
||||||
|
{
|
||||||
|
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||||
|
editor.updateSelfProfileKeyInGroup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
public static void acceptInvite(@NonNull Context context, @NonNull GroupId.V2 groupId)
|
||||||
|
throws GroupChangeBusyException, GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||||
|
{
|
||||||
|
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||||
|
editor.acceptInvite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public static void updateGroupTimer(@NonNull Context context, @NonNull GroupId.Push groupId, int expirationTime)
|
public static void updateGroupTimer(@NonNull Context context, @NonNull GroupId.Push groupId, int expirationTime)
|
||||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
|
||||||
{
|
{
|
||||||
if (groupId.isV2()) {
|
if (groupId.isV2()) {
|
||||||
new GroupManagerV2(context).edit(groupId.requireV2())
|
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||||
.updateGroupTimer(expirationTime);
|
editor.updateGroupTimer(expirationTime);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
GroupManagerV1.updateGroupTimer(context, groupId.requireV1(), expirationTime);
|
GroupManagerV1.updateGroupTimer(context, groupId.requireV1(), expirationTime);
|
||||||
}
|
}
|
||||||
@ -103,27 +139,53 @@ public final class GroupManager {
|
|||||||
public static void cancelInvites(@NonNull Context context,
|
public static void cancelInvites(@NonNull Context context,
|
||||||
@NonNull GroupId.V2 groupId,
|
@NonNull GroupId.V2 groupId,
|
||||||
@NonNull Collection<UuidCiphertext> uuidCipherTexts)
|
@NonNull Collection<UuidCiphertext> uuidCipherTexts)
|
||||||
throws InvalidGroupStateException, VerificationFailedException, IOException
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
|
||||||
{
|
{
|
||||||
throw new AssertionError("NYI"); // TODO: GV2 allow invite cancellation
|
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||||
|
editor.cancelInvites(uuidCipherTexts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public static void applyMembershipAdditionRightsChange(@NonNull Context context,
|
public static void applyMembershipAdditionRightsChange(@NonNull Context context,
|
||||||
@NonNull GroupId.V2 groupId,
|
@NonNull GroupId.V2 groupId,
|
||||||
@NonNull GroupAccessControl newRights)
|
@NonNull GroupAccessControl newRights)
|
||||||
throws GroupChangeFailedException, GroupInsufficientRightsException
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
|
||||||
{
|
{
|
||||||
throw new GroupChangeFailedException(new AssertionError("NYI")); // TODO: GV2 allow membership addition rights change
|
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||||
|
editor.updateMembershipRights(newRights);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public static void applyAttributesRightsChange(@NonNull Context context,
|
public static void applyAttributesRightsChange(@NonNull Context context,
|
||||||
@NonNull GroupId.V2 groupId,
|
@NonNull GroupId.V2 groupId,
|
||||||
@NonNull GroupAccessControl newRights)
|
@NonNull GroupAccessControl newRights)
|
||||||
throws GroupChangeFailedException, GroupInsufficientRightsException
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
|
||||||
{
|
{
|
||||||
throw new GroupChangeFailedException(new AssertionError("NYI")); // TODO: GV2 allow attributes rights change
|
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||||
|
editor.updateAttributesRights(newRights);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addMembers(@NonNull Context context,
|
||||||
|
@NonNull GroupId.Push groupId,
|
||||||
|
@NonNull Collection<RecipientId> newMembers)
|
||||||
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException, MembershipNotSuitableForV2Exception
|
||||||
|
{
|
||||||
|
if (groupId.isV2()) {
|
||||||
|
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||||
|
editor.addMembers(newMembers);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).requireGroup(groupId);
|
||||||
|
List<RecipientId> members = groupRecord.getMembers();
|
||||||
|
byte[] avatar = Util.readFully(AvatarHelper.getAvatar(context, groupRecord.getRecipientId()));
|
||||||
|
Set<RecipientId> addresses = new HashSet<>(members);
|
||||||
|
|
||||||
|
addresses.addAll(newMembers);
|
||||||
|
GroupManagerV1.updateGroup(context, groupId, addresses, avatar, groupRecord.getTitle());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class GroupActionResult {
|
public static class GroupActionResult {
|
||||||
|
@ -34,7 +34,6 @@ import org.thoughtcrime.securesms.util.BitmapUtil;
|
|||||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
@ -89,7 +88,6 @@ final class GroupManagerV1 {
|
|||||||
@NonNull Set<RecipientId> memberAddresses,
|
@NonNull Set<RecipientId> memberAddresses,
|
||||||
@Nullable byte[] avatarBytes,
|
@Nullable byte[] avatarBytes,
|
||||||
@Nullable String name)
|
@Nullable String name)
|
||||||
throws InvalidNumberException
|
|
||||||
{
|
{
|
||||||
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||||
final RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
final RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||||
|
@ -6,36 +6,51 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
|
import org.signal.storageservice.protos.groups.AccessControl;
|
||||||
import org.signal.storageservice.protos.groups.GroupChange;
|
import org.signal.storageservice.protos.groups.GroupChange;
|
||||||
|
import org.signal.storageservice.protos.groups.Member;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||||
import org.signal.zkgroup.VerificationFailedException;
|
import org.signal.zkgroup.VerificationFailedException;
|
||||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||||
|
import org.signal.zkgroup.groups.UuidCiphertext;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
|
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.groups.v2.GroupCandidateHelper;
|
||||||
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
|
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||||
|
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupChangeUtil;
|
import org.whispersystems.signalservice.api.groupsv2.GroupChangeUtil;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Authorization;
|
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
|
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException;
|
||||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
final class GroupManagerV2 {
|
final class GroupManagerV2 {
|
||||||
@ -49,6 +64,8 @@ final class GroupManagerV2 {
|
|||||||
private final GroupsV2Authorization authorization;
|
private final GroupsV2Authorization authorization;
|
||||||
private final GroupsV2StateProcessor groupsV2StateProcessor;
|
private final GroupsV2StateProcessor groupsV2StateProcessor;
|
||||||
private final UUID selfUuid;
|
private final UUID selfUuid;
|
||||||
|
private final GroupCandidateHelper groupCandidateHelper;
|
||||||
|
private final GroupsV2CapabilityChecker capabilityChecker;
|
||||||
|
|
||||||
GroupManagerV2(@NonNull Context context) {
|
GroupManagerV2(@NonNull Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
@ -58,6 +75,13 @@ final class GroupManagerV2 {
|
|||||||
this.authorization = ApplicationDependencies.getGroupsV2Authorization();
|
this.authorization = ApplicationDependencies.getGroupsV2Authorization();
|
||||||
this.groupsV2StateProcessor = ApplicationDependencies.getGroupsV2StateProcessor();
|
this.groupsV2StateProcessor = ApplicationDependencies.getGroupsV2StateProcessor();
|
||||||
this.selfUuid = Recipient.self().getUuid().get();
|
this.selfUuid = Recipient.self().getUuid().get();
|
||||||
|
this.groupCandidateHelper = new GroupCandidateHelper(context);
|
||||||
|
this.capabilityChecker = new GroupsV2CapabilityChecker();
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
GroupCreator create() throws GroupChangeBusyException {
|
||||||
|
return new GroupCreator(GroupsV2ProcessingLock.acquireGroupProcessingLock());
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@ -65,6 +89,68 @@ final class GroupManagerV2 {
|
|||||||
return new GroupEditor(groupId, GroupsV2ProcessingLock.acquireGroupProcessingLock());
|
return new GroupEditor(groupId, GroupsV2ProcessingLock.acquireGroupProcessingLock());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GroupCreator implements Closeable {
|
||||||
|
|
||||||
|
private final Closeable lock;
|
||||||
|
|
||||||
|
GroupCreator(@NonNull Closeable lock) {
|
||||||
|
this.lock = lock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
@NonNull GroupManager.GroupActionResult createGroup(@NonNull Collection<RecipientId> members,
|
||||||
|
@Nullable String name,
|
||||||
|
@Nullable byte[] avatar)
|
||||||
|
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
|
||||||
|
{
|
||||||
|
if (!capabilityChecker.allAndSelfSupportGroupsV2AndUuid(members)) {
|
||||||
|
throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 or UUID capabilities");
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupCandidate self = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
|
||||||
|
Set<GroupCandidate> candidates = new HashSet<>(groupCandidateHelper.recipientIdsToCandidates(members));
|
||||||
|
|
||||||
|
if (!self.hasProfileKeyCredential()) {
|
||||||
|
Log.w(TAG, "Cannot create a V2 group as self does not have a versioned profile");
|
||||||
|
throw new MembershipNotSuitableForV2Exception("Cannot create a V2 group as self does not have a versioned profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupsV2Operations.NewGroup newGroup = groupsV2Operations.createNewGroup(name,
|
||||||
|
Optional.fromNullable(avatar),
|
||||||
|
self,
|
||||||
|
candidates);
|
||||||
|
|
||||||
|
GroupSecretParams groupSecretParams = newGroup.getGroupSecretParams();
|
||||||
|
GroupMasterKey masterKey = groupSecretParams.getMasterKey();
|
||||||
|
|
||||||
|
try {
|
||||||
|
groupsV2Api.putNewGroup(newGroup, authorization.getAuthorizationForToday(Recipient.self().requireUuid(), groupSecretParams));
|
||||||
|
|
||||||
|
DecryptedGroup decryptedGroup = groupsV2Api.getGroup(groupSecretParams, ApplicationDependencies.getGroupsV2Authorization().getAuthorizationForToday(Recipient.self().requireUuid(), groupSecretParams));
|
||||||
|
if (decryptedGroup == null) {
|
||||||
|
throw new GroupChangeFailedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup);
|
||||||
|
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||||
|
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||||
|
|
||||||
|
AvatarHelper.setAvatar(context, groupRecipientId, avatar != null ? new ByteArrayInputStream(avatar) : null);
|
||||||
|
groupDatabase.onAvatarUpdated(groupId, avatar != null);
|
||||||
|
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true);
|
||||||
|
|
||||||
|
return sendGroupUpdate(masterKey, decryptedGroup, null);
|
||||||
|
} catch (VerificationFailedException | InvalidGroupStateException e) {
|
||||||
|
throw new GroupChangeFailedException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
lock.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class GroupEditor implements Closeable {
|
class GroupEditor implements Closeable {
|
||||||
|
|
||||||
private final Closeable lock;
|
private final Closeable lock;
|
||||||
@ -84,6 +170,18 @@ final class GroupManagerV2 {
|
|||||||
this.groupOperations = groupsV2Operations.forGroup(groupSecretParams);
|
this.groupOperations = groupsV2Operations.forGroup(groupSecretParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
@NonNull GroupManager.GroupActionResult addMembers(@NonNull Collection<RecipientId> newMembers)
|
||||||
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, MembershipNotSuitableForV2Exception
|
||||||
|
{
|
||||||
|
if (!capabilityChecker.allSupportGroupsV2AndUuid(newMembers)) {
|
||||||
|
throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 or UUID capabilities");
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<GroupCandidate> groupCandidates = groupCandidateHelper.recipientIdsToCandidates(new HashSet<>(newMembers));
|
||||||
|
return commitChangeWithConflictResolution(groupOperations.createModifyGroupMembershipChange(groupCandidates, selfUuid));
|
||||||
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@NonNull GroupManager.GroupActionResult updateGroupTimer(int expirationTime)
|
@NonNull GroupManager.GroupActionResult updateGroupTimer(int expirationTime)
|
||||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||||
@ -91,6 +189,113 @@ final class GroupManagerV2 {
|
|||||||
return commitChangeWithConflictResolution(groupOperations.createModifyGroupTimerChange(expirationTime));
|
return commitChangeWithConflictResolution(groupOperations.createModifyGroupTimerChange(expirationTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
@NonNull GroupManager.GroupActionResult updateAttributesRights(@NonNull GroupAccessControl newRights)
|
||||||
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||||
|
{
|
||||||
|
return commitChangeWithConflictResolution(groupOperations.createChangeAttributesRights(rightsToAccessControl(newRights)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
@NonNull GroupManager.GroupActionResult updateMembershipRights(@NonNull GroupAccessControl newRights)
|
||||||
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||||
|
{
|
||||||
|
return commitChangeWithConflictResolution(groupOperations.createChangeMembershipRights(rightsToAccessControl(newRights)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
@NonNull GroupManager.GroupActionResult updateGroupTitleAndAvatar(@Nullable String title, @Nullable byte[] avatarBytes)
|
||||||
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
GroupChange.Actions.Builder change = groupOperations.createModifyGroupTitleAndMembershipChange(Optional.fromNullable(title), Collections.emptySet(), Collections.emptySet());
|
||||||
|
|
||||||
|
if (avatarBytes != null) {
|
||||||
|
String cdnKey = groupsV2Api.uploadAvatar(avatarBytes, groupSecretParams, authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
|
||||||
|
change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder()
|
||||||
|
.setAvatar(cdnKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupManager.GroupActionResult groupActionResult = commitChangeWithConflictResolution(change);
|
||||||
|
|
||||||
|
if (avatarBytes != null) {
|
||||||
|
AvatarHelper.setAvatar(context, Recipient.externalGroup(context, groupId).getId(), new ByteArrayInputStream(avatarBytes));
|
||||||
|
groupDatabase.onAvatarUpdated(groupId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupActionResult;
|
||||||
|
} catch (VerificationFailedException e) {
|
||||||
|
throw new GroupChangeFailedException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
@NonNull GroupManager.GroupActionResult cancelInvites(@NonNull Collection<UuidCiphertext> uuidCipherTexts)
|
||||||
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||||
|
{
|
||||||
|
return commitChangeWithConflictResolution(groupOperations.createRemoveInvitationChange(new HashSet<>(uuidCipherTexts)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
@NonNull GroupManager.GroupActionResult setMemberAdmin(@NonNull RecipientId recipientId,
|
||||||
|
boolean admin)
|
||||||
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||||
|
{
|
||||||
|
Recipient recipient = Recipient.resolved(recipientId);
|
||||||
|
return commitChangeWithConflictResolution(groupOperations.createChangeMemberRole(recipient.getUuid().get(), admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
@Nullable GroupManager.GroupActionResult updateSelfProfileKeyInGroup()
|
||||||
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||||
|
{
|
||||||
|
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||||
|
DecryptedGroup group = groupDatabase.requireGroup(groupId).requireV2GroupProperties().getDecryptedGroup();
|
||||||
|
Optional<DecryptedMember> selfInGroup = DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfUuid);
|
||||||
|
|
||||||
|
if (!selfInGroup.isPresent()) {
|
||||||
|
Log.w(TAG, "Self not in group");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Arrays.equals(profileKey.serialize(), selfInGroup.get().getProfileKey().toByteArray())) {
|
||||||
|
Log.i(TAG, "Own Profile Key is already up to date in group " + groupId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupCandidate groupCandidate = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
|
||||||
|
|
||||||
|
if (!groupCandidate.hasProfileKeyCredential()) {
|
||||||
|
Log.w(TAG, "No credential available");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return commitChangeWithConflictResolution(groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.getProfileKeyCredential().get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
@Nullable GroupManager.GroupActionResult acceptInvite()
|
||||||
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||||
|
{
|
||||||
|
DecryptedGroup group = groupDatabase.requireGroup(groupId).requireV2GroupProperties().getDecryptedGroup();
|
||||||
|
Optional<DecryptedMember> selfInGroup = DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), Recipient.self().getUuid().get());
|
||||||
|
|
||||||
|
if (selfInGroup.isPresent()) {
|
||||||
|
Log.w(TAG, "Self already in group");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupCandidate groupCandidate = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
|
||||||
|
|
||||||
|
if (!groupCandidate.hasProfileKeyCredential()) {
|
||||||
|
Log.w(TAG, "No credential available");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return commitChangeWithConflictResolution(groupOperations.createAcceptInviteChange(groupCandidate.getProfileKeyCredential().get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
void updateLocalToServerVersion(int version)
|
void updateLocalToServerVersion(int version)
|
||||||
throws IOException, GroupNotAMemberException
|
throws IOException, GroupNotAMemberException
|
||||||
{
|
{
|
||||||
@ -98,7 +303,7 @@ final class GroupManagerV2 {
|
|||||||
.updateLocalGroupToRevision(version, System.currentTimeMillis());
|
.updateLocalGroupToRevision(version, System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
|
|
||||||
private GroupManager.GroupActionResult commitChangeWithConflictResolution(GroupChange.Actions.Builder change)
|
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull GroupChange.Actions.Builder change)
|
||||||
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||||
{
|
{
|
||||||
change.setSourceUuid(UuidUtil.toByteString(Recipient.self().getUuid().get()));
|
change.setSourceUuid(UuidUtil.toByteString(Recipient.self().getUuid().get()));
|
||||||
@ -106,8 +311,29 @@ final class GroupManagerV2 {
|
|||||||
for (int attempt = 0; attempt < 5; attempt++) {
|
for (int attempt = 0; attempt < 5; attempt++) {
|
||||||
try {
|
try {
|
||||||
return commitChange(change);
|
return commitChange(change);
|
||||||
|
} catch (GroupPatchNotAcceptedException e) {
|
||||||
|
throw new GroupChangeFailedException(e);
|
||||||
} catch (ConflictException e) {
|
} catch (ConflictException e) {
|
||||||
Log.w(TAG, "Conflict on group");
|
Log.w(TAG, "Invalid group patch or conflict", e);
|
||||||
|
|
||||||
|
change = resolveConflict(change);
|
||||||
|
|
||||||
|
if (GroupChangeUtil.changeIsEmpty(change.build())) {
|
||||||
|
Log.i(TAG, "Change is empty after conflict resolution");
|
||||||
|
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
|
||||||
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
||||||
|
|
||||||
|
return new GroupManager.GroupActionResult(groupRecipient, threadId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new GroupChangeFailedException("Unable to apply change to group after conflicts");
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupChange.Actions.Builder resolveConflict(@NonNull GroupChange.Actions.Builder change)
|
||||||
|
throws IOException, GroupNotAMemberException, GroupChangeFailedException
|
||||||
|
{
|
||||||
GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(groupMasterKey)
|
GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(groupMasterKey)
|
||||||
.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis());
|
.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis());
|
||||||
|
|
||||||
@ -117,19 +343,17 @@ final class GroupManagerV2 {
|
|||||||
|
|
||||||
Log.w(TAG, "Group has been updated");
|
Log.w(TAG, "Group has been updated");
|
||||||
try {
|
try {
|
||||||
change = GroupChangeUtil.resolveConflict(groupUpdateResult.getLatestServer(),
|
GroupChange.Actions changeActions = change.build();
|
||||||
groupOperations.decryptChange(change.build(), selfUuid),
|
|
||||||
change.build());
|
return GroupChangeUtil.resolveConflict(groupUpdateResult.getLatestServer(),
|
||||||
|
groupOperations.decryptChange(changeActions, selfUuid),
|
||||||
|
changeActions);
|
||||||
} catch (VerificationFailedException | InvalidGroupStateException ex) {
|
} catch (VerificationFailedException | InvalidGroupStateException ex) {
|
||||||
throw new GroupChangeFailedException(ex);
|
throw new GroupChangeFailedException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
throw new GroupChangeFailedException("Unable to apply change to group after conflicts");
|
private GroupManager.GroupActionResult commitChange(@NonNull GroupChange.Actions.Builder change)
|
||||||
}
|
|
||||||
|
|
||||||
private GroupManager.GroupActionResult commitChange(GroupChange.Actions.Builder change)
|
|
||||||
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
|
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
|
||||||
{
|
{
|
||||||
final GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
|
final GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
|
||||||
@ -157,7 +381,7 @@ final class GroupManagerV2 {
|
|||||||
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
|
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
groupsV2Api.patchGroup(change, groupSecretParams, authorization);
|
groupsV2Api.patchGroup(change, groupSecretParams, authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
|
||||||
} catch (NotInGroupException e) {
|
} catch (NotInGroupException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
throw new GroupNotAMemberException(e);
|
throw new GroupNotAMemberException(e);
|
||||||
@ -170,12 +394,18 @@ final class GroupManagerV2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
lock.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private @NonNull GroupManager.GroupActionResult sendGroupUpdate(@NonNull GroupMasterKey masterKey,
|
private @NonNull GroupManager.GroupActionResult sendGroupUpdate(@NonNull GroupMasterKey masterKey,
|
||||||
@NonNull DecryptedGroup decryptedGroup,
|
@NonNull DecryptedGroup decryptedGroup,
|
||||||
@Nullable DecryptedGroupChange plainGroupChange)
|
@Nullable DecryptedGroupChange plainGroupChange)
|
||||||
{
|
{
|
||||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
GroupId.V2 groupId = GroupId.v2(masterKey);
|
||||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
|
||||||
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, decryptedGroup, plainGroupChange);
|
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, decryptedGroup, plainGroupChange);
|
||||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient,
|
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient,
|
||||||
decryptedGroupV2Context,
|
decryptedGroupV2Context,
|
||||||
@ -192,9 +422,14 @@ final class GroupManagerV2 {
|
|||||||
return new GroupManager.GroupActionResult(groupRecipient, threadId);
|
return new GroupManager.GroupActionResult(groupRecipient, threadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private static @NonNull AccessControl.AccessRequired rightsToAccessControl(@NonNull GroupAccessControl rights) {
|
||||||
public void close() throws IOException {
|
switch (rights){
|
||||||
lock.close();
|
case ALL_MEMBERS:
|
||||||
|
return AccessControl.AccessRequired.MEMBER;
|
||||||
|
case ONLY_ADMINS:
|
||||||
|
return AccessControl.AccessRequired.ADMINISTRATOR;
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.signal.zkgroup.VerificationFailedException;
|
||||||
|
import org.signal.zkgroup.auth.AuthCredentialResponse;
|
||||||
|
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public final class GroupsV2Authorization {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(GroupsV2Authorization.class);
|
||||||
|
|
||||||
|
private final ValueCache cache;
|
||||||
|
private final GroupsV2Api groupsV2Api;
|
||||||
|
|
||||||
|
public GroupsV2Authorization(@NonNull GroupsV2Api groupsV2Api, @NonNull ValueCache cache) {
|
||||||
|
this.groupsV2Api = groupsV2Api;
|
||||||
|
this.cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GroupsV2AuthorizationString getAuthorizationForToday(@NonNull UUID self,
|
||||||
|
@NonNull GroupSecretParams groupSecretParams)
|
||||||
|
throws IOException, VerificationFailedException
|
||||||
|
{
|
||||||
|
final int today = currentTimeDays();
|
||||||
|
|
||||||
|
Map<Integer, AuthCredentialResponse> credentials = cache.read();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return getAuthorization(self, groupSecretParams, credentials, today);
|
||||||
|
} catch (NoCredentialForRedemptionTimeException e) {
|
||||||
|
Log.i(TAG, "Auth out of date, will update auth and try again");
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Getting new auth credential responses");
|
||||||
|
credentials = groupsV2Api.getCredentials(today);
|
||||||
|
cache.write(credentials);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return getAuthorization(self, groupSecretParams, credentials, today);
|
||||||
|
} catch (NoCredentialForRedemptionTimeException e) {
|
||||||
|
Log.w(TAG, "The credentials returned did not include the day requested");
|
||||||
|
throw new IOException("Failed to get credentials");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int currentTimeDays() {
|
||||||
|
return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupsV2AuthorizationString getAuthorization(UUID self,
|
||||||
|
GroupSecretParams groupSecretParams,
|
||||||
|
Map<Integer, AuthCredentialResponse> credentials,
|
||||||
|
int today)
|
||||||
|
throws NoCredentialForRedemptionTimeException, VerificationFailedException
|
||||||
|
{
|
||||||
|
AuthCredentialResponse authCredentialResponse = credentials.get(today);
|
||||||
|
|
||||||
|
if (authCredentialResponse == null) {
|
||||||
|
throw new NoCredentialForRedemptionTimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupsV2Api.getGroupsV2AuthorizationString(self, today, groupSecretParams, authCredentialResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ValueCache {
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
@NonNull Map<Integer, AuthCredentialResponse> read();
|
||||||
|
|
||||||
|
void write(@NonNull Map<Integer, AuthCredentialResponse> values);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.signal.zkgroup.auth.AuthCredentialResponse;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class GroupsV2AuthorizationMemoryValueCache implements GroupsV2Authorization.ValueCache {
|
||||||
|
|
||||||
|
private final GroupsV2Authorization.ValueCache inner;
|
||||||
|
private Map<Integer, AuthCredentialResponse> values;
|
||||||
|
|
||||||
|
public GroupsV2AuthorizationMemoryValueCache(@NonNull GroupsV2Authorization.ValueCache inner) {
|
||||||
|
this.inner = inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void clear() {
|
||||||
|
inner.clear();
|
||||||
|
values = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull synchronized Map<Integer, AuthCredentialResponse> read() {
|
||||||
|
Map<Integer, AuthCredentialResponse> map = values;
|
||||||
|
|
||||||
|
if (map == null) {
|
||||||
|
map = inner.read();
|
||||||
|
values = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void write(@NonNull Map<Integer, AuthCredentialResponse> values) {
|
||||||
|
inner.write(values);
|
||||||
|
this.values = Collections.unmodifiableMap(new HashMap<>(values));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
final class GroupsV2CapabilityChecker {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(GroupsV2CapabilityChecker.class);
|
||||||
|
|
||||||
|
GroupsV2CapabilityChecker() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
boolean allAndSelfSupportGroupsV2AndUuid(@NonNull Collection<RecipientId> recipientIds)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
HashSet<RecipientId> recipientIdsSet = new HashSet<>(recipientIds);
|
||||||
|
|
||||||
|
recipientIdsSet.add(Recipient.self().getId());
|
||||||
|
|
||||||
|
return allSupportGroupsV2AndUuid(recipientIdsSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
boolean allSupportGroupsV2AndUuid(@NonNull Collection<RecipientId> recipientIds)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
final HashSet<RecipientId> recipientIdsSet = new HashSet<>(recipientIds);
|
||||||
|
|
||||||
|
for (RecipientId recipientId : recipientIdsSet) {
|
||||||
|
Recipient member = Recipient.resolved(recipientId);
|
||||||
|
Recipient.Capability gv2Capability = member.getGroupsV2Capability();
|
||||||
|
Recipient.Capability uuidCapability = member.getUuidCapability();
|
||||||
|
|
||||||
|
if (gv2Capability == Recipient.Capability.UNKNOWN || uuidCapability == Recipient.Capability.UNKNOWN) {
|
||||||
|
if (!ApplicationDependencies.getJobManager().runSynchronously(RetrieveProfileJob.forRecipient(member), TimeUnit.SECONDS.toMillis(1000)).isPresent()) {
|
||||||
|
throw new IOException("Recipient capability was not retrieved in time");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gv2Capability != Recipient.Capability.SUPPORTED) {
|
||||||
|
Log.i(TAG, "At least one recipient does not support GV2, capability was " + gv2Capability);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uuidCapability != Recipient.Capability.SUPPORTED) {
|
||||||
|
Log.i(TAG, "At least one recipient does not support UUID, capability was " + uuidCapability);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (RecipientId recipientId : recipientIdsSet) {
|
||||||
|
Recipient member = Recipient.resolved(recipientId);
|
||||||
|
|
||||||
|
if (!member.hasUuid()) {
|
||||||
|
Log.i(TAG, "At least one recipient did not have a UUID known to us");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -91,31 +91,41 @@ public final class LiveGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<Boolean> selfCanEditGroupAttributes() {
|
public LiveData<Boolean> selfCanEditGroupAttributes() {
|
||||||
return LiveDataUtil.combineLatest(isSelfAdmin(),
|
return LiveDataUtil.combineLatest(isSelfAdmin(), getAttributesAccessControl(), this::applyAccessControl);
|
||||||
getAttributesAccessControl(),
|
|
||||||
(admin, rights) -> {
|
|
||||||
switch (rights) {
|
|
||||||
case ALL_MEMBERS:
|
|
||||||
return true;
|
|
||||||
case ONLY_ADMINS:
|
|
||||||
return admin;
|
|
||||||
default:
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LiveData<Boolean> selfCanAddMembers() {
|
||||||
|
return LiveDataUtil.combineLatest(isSelfAdmin(), getMembershipAdditionAccessControl(), this::applyAccessControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string representing the count of full members and pending members if > 0.
|
||||||
|
*/
|
||||||
public LiveData<String> getMembershipCountDescription(@NonNull Resources resources) {
|
public LiveData<String> getMembershipCountDescription(@NonNull Resources resources) {
|
||||||
return LiveDataUtil.combineLatest(getFullMembers(),
|
return LiveDataUtil.combineLatest(getFullMembers(),
|
||||||
getPendingMemberCount(),
|
getPendingMemberCount(),
|
||||||
(fullMembers, invitedCount) -> getMembershipDescription(resources, invitedCount, fullMembers.size()));
|
(fullMembers, invitedCount) -> getMembershipDescription(resources, invitedCount, fullMembers.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string representing the count of full members.
|
||||||
|
*/
|
||||||
|
public LiveData<String> getFullMembershipCountDescription(@NonNull Resources resources) {
|
||||||
|
return Transformations.map(getFullMembers(), fullMembers -> getMembershipDescription(resources, 0, fullMembers.size()));
|
||||||
|
}
|
||||||
|
|
||||||
private static String getMembershipDescription(@NonNull Resources resources, int invitedCount, int fullMemberCount) {
|
private static String getMembershipDescription(@NonNull Resources resources, int invitedCount, int fullMemberCount) {
|
||||||
return invitedCount > 0 ? resources.getQuantityString(R.plurals.MessageRequestProfileView_members_and_invited, fullMemberCount,
|
return invitedCount > 0 ? resources.getQuantityString(R.plurals.MessageRequestProfileView_members_and_invited, fullMemberCount,
|
||||||
fullMemberCount, invitedCount)
|
fullMemberCount, invitedCount)
|
||||||
: resources.getQuantityString(R.plurals.MessageRequestProfileView_members, fullMemberCount,
|
: resources.getQuantityString(R.plurals.MessageRequestProfileView_members, fullMemberCount,
|
||||||
fullMemberCount);
|
fullMemberCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean applyAccessControl(boolean isAdmin, @NonNull GroupAccessControl rights) {
|
||||||
|
switch (rights) {
|
||||||
|
case ALL_MEMBERS: return true;
|
||||||
|
case ONLY_ADMINS: return isAdmin;
|
||||||
|
default: throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups;
|
||||||
|
|
||||||
|
public final class MembershipNotSuitableForV2Exception extends Exception {
|
||||||
|
public MembershipNotSuitableForV2Exception(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -23,11 +23,14 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||||
import org.thoughtcrime.securesms.MuteDialog;
|
import org.thoughtcrime.securesms.MuteDialog;
|
||||||
|
import org.thoughtcrime.securesms.PushContactSelectionActivity;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||||
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
|
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
|
||||||
|
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||||
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
|
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
|
||||||
@ -39,9 +42,11 @@ import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
|||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@ -51,12 +56,14 @@ public class ManageGroupFragment extends Fragment {
|
|||||||
private static final String TAG = Log.tag(ManageGroupFragment.class);
|
private static final String TAG = Log.tag(ManageGroupFragment.class);
|
||||||
|
|
||||||
private static final int RETURN_FROM_MEDIA = 33114;
|
private static final int RETURN_FROM_MEDIA = 33114;
|
||||||
|
private static final int PICK_CONTACT = 61341;
|
||||||
|
|
||||||
private ManageGroupViewModel viewModel;
|
private ManageGroupViewModel viewModel;
|
||||||
private GroupMemberListView groupMemberList;
|
private GroupMemberListView groupMemberList;
|
||||||
private View listPending;
|
private View listPending;
|
||||||
private TextView groupTitle;
|
private TextView groupTitle;
|
||||||
private TextView memberCount;
|
private TextView memberCountUnderAvatar;
|
||||||
|
private TextView memberCountAboveList;
|
||||||
private AvatarImageView avatar;
|
private AvatarImageView avatar;
|
||||||
private ThreadPhotoRailView threadPhotoRailView;
|
private ThreadPhotoRailView threadPhotoRailView;
|
||||||
private View groupMediaCard;
|
private View groupMediaCard;
|
||||||
@ -69,6 +76,7 @@ public class ManageGroupFragment extends Fragment {
|
|||||||
private Button disappearingMessages;
|
private Button disappearingMessages;
|
||||||
private Button blockGroup;
|
private Button blockGroup;
|
||||||
private Button leaveGroup;
|
private Button leaveGroup;
|
||||||
|
private Button addMembers;
|
||||||
private Switch muteNotificationsSwitch;
|
private Switch muteNotificationsSwitch;
|
||||||
private TextView muteNotificationsUntilLabel;
|
private TextView muteNotificationsUntilLabel;
|
||||||
private TextView customNotificationsButton;
|
private TextView customNotificationsButton;
|
||||||
@ -99,7 +107,8 @@ public class ManageGroupFragment extends Fragment {
|
|||||||
|
|
||||||
avatar = view.findViewById(R.id.group_avatar);
|
avatar = view.findViewById(R.id.group_avatar);
|
||||||
groupTitle = view.findViewById(R.id.group_title);
|
groupTitle = view.findViewById(R.id.group_title);
|
||||||
memberCount = view.findViewById(R.id.member_count);
|
memberCountUnderAvatar = view.findViewById(R.id.member_count);
|
||||||
|
memberCountAboveList = view.findViewById(R.id.member_count_2);
|
||||||
groupMemberList = view.findViewById(R.id.group_members);
|
groupMemberList = view.findViewById(R.id.group_members);
|
||||||
listPending = view.findViewById(R.id.listPending);
|
listPending = view.findViewById(R.id.listPending);
|
||||||
threadPhotoRailView = view.findViewById(R.id.recent_photos);
|
threadPhotoRailView = view.findViewById(R.id.recent_photos);
|
||||||
@ -112,6 +121,7 @@ public class ManageGroupFragment extends Fragment {
|
|||||||
disappearingMessages = view.findViewById(R.id.disappearing_messages);
|
disappearingMessages = view.findViewById(R.id.disappearing_messages);
|
||||||
blockGroup = view.findViewById(R.id.blockGroup);
|
blockGroup = view.findViewById(R.id.blockGroup);
|
||||||
leaveGroup = view.findViewById(R.id.leaveGroup);
|
leaveGroup = view.findViewById(R.id.leaveGroup);
|
||||||
|
addMembers = view.findViewById(R.id.add_members);
|
||||||
muteNotificationsUntilLabel = view.findViewById(R.id.group_mute_notifications_until);
|
muteNotificationsUntilLabel = view.findViewById(R.id.group_mute_notifications_until);
|
||||||
muteNotificationsSwitch = view.findViewById(R.id.group_mute_notifications_switch);
|
muteNotificationsSwitch = view.findViewById(R.id.group_mute_notifications_switch);
|
||||||
customNotificationsButton = view.findViewById(R.id.group_custom_notifications_button);
|
customNotificationsButton = view.findViewById(R.id.group_custom_notifications_button);
|
||||||
@ -147,12 +157,13 @@ public class ManageGroupFragment extends Fragment {
|
|||||||
});
|
});
|
||||||
|
|
||||||
viewModel.getTitle().observe(getViewLifecycleOwner(), groupTitle::setText);
|
viewModel.getTitle().observe(getViewLifecycleOwner(), groupTitle::setText);
|
||||||
viewModel.getMemberCountSummary().observe(getViewLifecycleOwner(), memberCount::setText);
|
viewModel.getMemberCountSummary().observe(getViewLifecycleOwner(), memberCountUnderAvatar::setText);
|
||||||
|
viewModel.getFullMemberCountSummary().observe(getViewLifecycleOwner(), memberCountAboveList::setText);
|
||||||
|
viewModel.getGroupRecipient().observe(getViewLifecycleOwner(), avatar::setRecipient);
|
||||||
|
|
||||||
viewModel.getGroupViewState().observe(getViewLifecycleOwner(), vs -> {
|
viewModel.getGroupViewState().observe(getViewLifecycleOwner(), vs -> {
|
||||||
if (vs == null) return;
|
if (vs == null) return;
|
||||||
photoRailLabel.setOnClickListener(v -> startActivity(MediaOverviewActivity.forThread(context, vs.getThreadId())));
|
photoRailLabel.setOnClickListener(v -> startActivity(MediaOverviewActivity.forThread(context, vs.getThreadId())));
|
||||||
avatar.setRecipient(vs.getGroupRecipient());
|
|
||||||
|
|
||||||
setMediaCursorFactory(vs.getMediaCursorFactory());
|
setMediaCursorFactory(vs.getMediaCursorFactory());
|
||||||
|
|
||||||
@ -177,6 +188,12 @@ public class ManageGroupFragment extends Fragment {
|
|||||||
disappearingMessages.setOnClickListener(v -> viewModel.handleExpirationSelection());
|
disappearingMessages.setOnClickListener(v -> viewModel.handleExpirationSelection());
|
||||||
blockGroup.setOnClickListener(v -> viewModel.blockAndLeave(requireActivity()));
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
viewModel.getMembershipRights().observe(getViewLifecycleOwner(), r -> {
|
viewModel.getMembershipRights().observe(getViewLifecycleOwner(), r -> {
|
||||||
if (r != null) {
|
if (r != null) {
|
||||||
editGroupMembershipValue.setText(r.getString());
|
editGroupMembershipValue.setText(r.getString());
|
||||||
@ -199,6 +216,7 @@ public class ManageGroupFragment extends Fragment {
|
|||||||
});
|
});
|
||||||
|
|
||||||
viewModel.getCanEditGroupAttributes().observe(getViewLifecycleOwner(), canEdit -> disappearingMessages.setEnabled(canEdit));
|
viewModel.getCanEditGroupAttributes().observe(getViewLifecycleOwner(), canEdit -> disappearingMessages.setEnabled(canEdit));
|
||||||
|
viewModel.getCanAddMembers().observe(getViewLifecycleOwner(), canEdit -> addMembers.setVisibility(canEdit ? View.VISIBLE : View.GONE));
|
||||||
|
|
||||||
groupMemberList.setRecipientClickListener(recipient -> RecipientBottomSheetDialogFragment.create(recipient.getId(), groupId).show(requireFragmentManager(), "BOTTOM"));
|
groupMemberList.setRecipientClickListener(recipient -> RecipientBottomSheetDialogFragment.create(recipient.getId(), groupId).show(requireFragmentManager(), "BOTTOM"));
|
||||||
|
|
||||||
@ -290,6 +308,9 @@ public class ManageGroupFragment extends Fragment {
|
|||||||
super.onActivityResult(requestCode, resultCode, data);
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
if (requestCode == RETURN_FROM_MEDIA) {
|
if (requestCode == RETURN_FROM_MEDIA) {
|
||||||
applyMediaCursorFactory();
|
applyMediaCursorFactory();
|
||||||
|
} else if (requestCode == PICK_CONTACT) {
|
||||||
|
List<RecipientId> selected = data.getParcelableArrayListExtra(PushContactSelectionActivity.KEY_SELECTED_RECIPIENTS);
|
||||||
|
viewModel.onAddMembers(selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.groups.GroupId;
|
|||||||
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
|
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||||
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
||||||
|
import org.thoughtcrime.securesms.groups.MembershipNotSuitableForV2Exception;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
@ -24,6 +25,7 @@ import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
|||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
final class ManageGroupRepository {
|
final class ManageGroupRepository {
|
||||||
@ -31,12 +33,10 @@ final class ManageGroupRepository {
|
|||||||
private static final String TAG = Log.tag(ManageGroupRepository.class);
|
private static final String TAG = Log.tag(ManageGroupRepository.class);
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final GroupId groupId;
|
private final GroupId.Push groupId;
|
||||||
private final ExecutorService executor;
|
|
||||||
|
|
||||||
ManageGroupRepository(@NonNull Context context, @NonNull GroupId groupId) {
|
ManageGroupRepository(@NonNull Context context, @NonNull GroupId.Push groupId) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.executor = SignalExecutors.BOUNDED;
|
|
||||||
this.groupId = groupId;
|
this.groupId = groupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ final class ManageGroupRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void getGroupState(@NonNull Consumer<GroupStateResult> onGroupStateLoaded) {
|
void getGroupState(@NonNull Consumer<GroupStateResult> onGroupStateLoaded) {
|
||||||
executor.execute(() -> onGroupStateLoaded.accept(getGroupState()));
|
SignalExecutors.BOUNDED.execute(() -> onGroupStateLoaded.accept(getGroupState()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@ -58,7 +58,7 @@ final class ManageGroupRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setExpiration(int newExpirationTime, @NonNull Error error) {
|
void setExpiration(int newExpirationTime, @NonNull Error error) {
|
||||||
SignalExecutors.BOUNDED.execute(() -> {
|
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||||
try {
|
try {
|
||||||
GroupManager.updateGroupTimer(context, groupId.requirePush(), newExpirationTime);
|
GroupManager.updateGroupTimer(context, groupId.requirePush(), newExpirationTime);
|
||||||
} catch (GroupInsufficientRightsException e) {
|
} catch (GroupInsufficientRightsException e) {
|
||||||
@ -75,13 +75,13 @@ final class ManageGroupRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void applyMembershipRightsChange(@NonNull GroupAccessControl newRights, @NonNull Error error) {
|
void applyMembershipRightsChange(@NonNull GroupAccessControl newRights, @NonNull Error error) {
|
||||||
SignalExecutors.BOUNDED.execute(() -> {
|
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||||
try {
|
try {
|
||||||
GroupManager.applyMembershipAdditionRightsChange(context, groupId.requireV2(), newRights);
|
GroupManager.applyMembershipAdditionRightsChange(context, groupId.requireV2(), newRights);
|
||||||
} catch (GroupInsufficientRightsException e) {
|
} catch (GroupInsufficientRightsException | GroupNotAMemberException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
error.onError(FailureReason.NO_RIGHTS);
|
error.onError(FailureReason.NO_RIGHTS);
|
||||||
} catch (GroupChangeFailedException e) {
|
} catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
error.onError(FailureReason.OTHER);
|
error.onError(FailureReason.OTHER);
|
||||||
}
|
}
|
||||||
@ -89,13 +89,13 @@ final class ManageGroupRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void applyAttributesRightsChange(@NonNull GroupAccessControl newRights, @NonNull Error error) {
|
void applyAttributesRightsChange(@NonNull GroupAccessControl newRights, @NonNull Error error) {
|
||||||
SignalExecutors.BOUNDED.execute(() -> {
|
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||||
try {
|
try {
|
||||||
GroupManager.applyAttributesRightsChange(context, groupId.requireV2(), newRights);
|
GroupManager.applyAttributesRightsChange(context, groupId.requireV2(), newRights);
|
||||||
} catch (GroupInsufficientRightsException e) {
|
} catch (GroupInsufficientRightsException | GroupNotAMemberException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
error.onError(FailureReason.NO_RIGHTS);
|
error.onError(FailureReason.NO_RIGHTS);
|
||||||
} catch (GroupChangeFailedException e) {
|
} catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
error.onError(FailureReason.OTHER);
|
error.onError(FailureReason.OTHER);
|
||||||
}
|
}
|
||||||
@ -108,13 +108,30 @@ final class ManageGroupRepository {
|
|||||||
recipientCallback::accept);
|
recipientCallback::accept);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMuteUntil(long until) {
|
void setMuteUntil(long until) {
|
||||||
SignalExecutors.BOUNDED.execute(() -> {
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
RecipientId recipientId = Recipient.externalGroup(context, groupId).getId();
|
RecipientId recipientId = Recipient.externalGroup(context, groupId).getId();
|
||||||
DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until);
|
DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addMembers(@NonNull List<RecipientId> selected, @NonNull Error error) {
|
||||||
|
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||||
|
try {
|
||||||
|
GroupManager.addMembers(context, groupId, selected);
|
||||||
|
} catch (GroupInsufficientRightsException | GroupNotAMemberException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
error.onError(FailureReason.NO_RIGHTS);
|
||||||
|
} catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
error.onError(FailureReason.OTHER);
|
||||||
|
} catch (MembershipNotSuitableForV2Exception e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
error.onError(FailureReason.NOT_CAPABLE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static final class GroupStateResult {
|
static final class GroupStateResult {
|
||||||
|
|
||||||
private final long threadId;
|
private final long threadId;
|
||||||
@ -138,6 +155,7 @@ final class ManageGroupRepository {
|
|||||||
|
|
||||||
public enum FailureReason {
|
public enum FailureReason {
|
||||||
NO_RIGHTS(R.string.ManageGroupActivity_you_dont_have_the_rights_to_do_this),
|
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),
|
NOT_A_MEMBER(R.string.ManageGroupActivity_youre_not_a_member_of_the_group),
|
||||||
OTHER(R.string.ManageGroupActivity_failed_to_update_the_group);
|
OTHER(R.string.ManageGroupActivity_failed_to_update_the_group);
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.groups.ui.managegroup;
|
package org.thoughtcrime.securesms.groups.ui.managegroup;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
@ -14,7 +15,11 @@ import androidx.lifecycle.ViewModel;
|
|||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.BlockUnblockDialog;
|
import org.thoughtcrime.securesms.BlockUnblockDialog;
|
||||||
|
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||||
import org.thoughtcrime.securesms.ExpirationDialog;
|
import org.thoughtcrime.securesms.ExpirationDialog;
|
||||||
|
import org.thoughtcrime.securesms.GroupCreateActivity;
|
||||||
|
import org.thoughtcrime.securesms.PushContactSelectionActivity;
|
||||||
|
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||||
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
|
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
|
||||||
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
|
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
|
||||||
@ -23,6 +28,7 @@ import org.thoughtcrime.securesms.groups.GroupId;
|
|||||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
@ -36,12 +42,15 @@ public class ManageGroupViewModel extends ViewModel {
|
|||||||
private final LiveData<String> title;
|
private final LiveData<String> title;
|
||||||
private final LiveData<Boolean> isAdmin;
|
private final LiveData<Boolean> isAdmin;
|
||||||
private final LiveData<Boolean> canEditGroupAttributes;
|
private final LiveData<Boolean> canEditGroupAttributes;
|
||||||
|
private final LiveData<Boolean> canAddMembers;
|
||||||
private final LiveData<List<GroupMemberEntry.FullMember>> members;
|
private final LiveData<List<GroupMemberEntry.FullMember>> members;
|
||||||
private final LiveData<Integer> pendingMemberCount;
|
private final LiveData<Integer> pendingMemberCount;
|
||||||
private final LiveData<String> disappearingMessageTimer;
|
private final LiveData<String> disappearingMessageTimer;
|
||||||
private final LiveData<String> memberCountSummary;
|
private final LiveData<String> memberCountSummary;
|
||||||
|
private final LiveData<String> fullMemberCountSummary;
|
||||||
private final LiveData<GroupAccessControl> editMembershipRights;
|
private final LiveData<GroupAccessControl> editMembershipRights;
|
||||||
private final LiveData<GroupAccessControl> editGroupAttributesRights;
|
private final LiveData<GroupAccessControl> editGroupAttributesRights;
|
||||||
|
private final LiveData<Recipient> groupRecipient;
|
||||||
private final MutableLiveData<GroupViewState> groupViewState = new MutableLiveData<>(null);
|
private final MutableLiveData<GroupViewState> groupViewState = new MutableLiveData<>(null);
|
||||||
private final LiveData<MuteState> muteState;
|
private final LiveData<MuteState> muteState;
|
||||||
private final LiveData<Boolean> hasCustomNotifications;
|
private final LiveData<Boolean> hasCustomNotifications;
|
||||||
@ -59,13 +68,16 @@ public class ManageGroupViewModel extends ViewModel {
|
|||||||
this.members = liveGroup.getFullMembers();
|
this.members = liveGroup.getFullMembers();
|
||||||
this.pendingMemberCount = liveGroup.getPendingMemberCount();
|
this.pendingMemberCount = liveGroup.getPendingMemberCount();
|
||||||
this.memberCountSummary = liveGroup.getMembershipCountDescription(context.getResources());
|
this.memberCountSummary = liveGroup.getMembershipCountDescription(context.getResources());
|
||||||
|
this.fullMemberCountSummary = liveGroup.getFullMembershipCountDescription(context.getResources());
|
||||||
this.editMembershipRights = liveGroup.getMembershipAdditionAccessControl();
|
this.editMembershipRights = liveGroup.getMembershipAdditionAccessControl();
|
||||||
this.editGroupAttributesRights = liveGroup.getAttributesAccessControl();
|
this.editGroupAttributesRights = liveGroup.getAttributesAccessControl();
|
||||||
this.disappearingMessageTimer = Transformations.map(liveGroup.getExpireMessages(), expiration -> ExpirationUtil.getExpirationDisplayValue(context, expiration));
|
this.disappearingMessageTimer = Transformations.map(liveGroup.getExpireMessages(), expiration -> ExpirationUtil.getExpirationDisplayValue(context, expiration));
|
||||||
this.canEditGroupAttributes = liveGroup.selfCanEditGroupAttributes();
|
this.canEditGroupAttributes = liveGroup.selfCanEditGroupAttributes();
|
||||||
this.muteState = Transformations.map(liveGroup.getGroupRecipient(),
|
this.canAddMembers = liveGroup.selfCanAddMembers();
|
||||||
|
this.groupRecipient = liveGroup.getGroupRecipient();
|
||||||
|
this.muteState = Transformations.map(this.groupRecipient,
|
||||||
recipient -> new MuteState(recipient.getMuteUntil(), recipient.isMuted()));
|
recipient -> new MuteState(recipient.getMuteUntil(), recipient.isMuted()));
|
||||||
this.hasCustomNotifications = Transformations.map(liveGroup.getGroupRecipient(),
|
this.hasCustomNotifications = Transformations.map(this.groupRecipient,
|
||||||
recipient -> recipient.getNotificationChannel() != null);
|
recipient -> recipient.getNotificationChannel() != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,6 +100,14 @@ public class ManageGroupViewModel extends ViewModel {
|
|||||||
return memberCountSummary;
|
return memberCountSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LiveData<String> getFullMemberCountSummary() {
|
||||||
|
return fullMemberCountSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Recipient> getGroupRecipient() {
|
||||||
|
return groupRecipient;
|
||||||
|
}
|
||||||
|
|
||||||
LiveData<GroupViewState> getGroupViewState() {
|
LiveData<GroupViewState> getGroupViewState() {
|
||||||
return groupViewState;
|
return groupViewState;
|
||||||
}
|
}
|
||||||
@ -116,6 +136,10 @@ public class ManageGroupViewModel extends ViewModel {
|
|||||||
return canEditGroupAttributes;
|
return canEditGroupAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LiveData<Boolean> getCanAddMembers() {
|
||||||
|
return canAddMembers;
|
||||||
|
}
|
||||||
|
|
||||||
LiveData<String> getDisappearingMessageTimer() {
|
LiveData<String> getDisappearingMessageTimer() {
|
||||||
return disappearingMessageTimer;
|
return disappearingMessageTimer;
|
||||||
}
|
}
|
||||||
@ -144,6 +168,10 @@ public class ManageGroupViewModel extends ViewModel {
|
|||||||
() -> RecipientUtil.block(context, recipient)));
|
() -> RecipientUtil.block(context, recipient)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onAddMembers(List<RecipientId> selected) {
|
||||||
|
manageGroupRepository.addMembers(selected, this::showErrorToast);
|
||||||
|
}
|
||||||
|
|
||||||
void setMuteUntil(long muteUntil) {
|
void setMuteUntil(long muteUntil) {
|
||||||
manageGroupRepository.setMuteUntil(muteUntil);
|
manageGroupRepository.setMuteUntil(muteUntil);
|
||||||
}
|
}
|
||||||
@ -154,7 +182,7 @@ public class ManageGroupViewModel extends ViewModel {
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private void showErrorToast(@NonNull ManageGroupRepository.FailureReason e) {
|
private void showErrorToast(@NonNull ManageGroupRepository.FailureReason e) {
|
||||||
Util.runOnMain(() -> Toast.makeText(context, e.getToastMessage(), Toast.LENGTH_SHORT).show());
|
Util.runOnMain(() -> Toast.makeText(context, e.getToastMessage(), Toast.LENGTH_LONG).show());
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class GroupViewState {
|
static final class GroupViewState {
|
||||||
|
@ -12,24 +12,25 @@ import com.google.protobuf.ByteString;
|
|||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||||
import org.signal.zkgroup.InvalidInputException;
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
import org.signal.zkgroup.VerificationFailedException;
|
|
||||||
import org.signal.zkgroup.groups.UuidCiphertext;
|
import org.signal.zkgroup.groups.UuidCiphertext;
|
||||||
import org.signal.zkgroup.util.UUIDUtil;
|
import org.signal.zkgroup.util.UUIDUtil;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
||||||
import org.thoughtcrime.securesms.groups.GroupProtoUtil;
|
import org.thoughtcrime.securesms.groups.GroupProtoUtil;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -104,7 +105,7 @@ final class PendingMemberRepository {
|
|||||||
try {
|
try {
|
||||||
GroupManager.cancelInvites(context, groupId, uuidCipherTexts);
|
GroupManager.cancelInvites(context, groupId, uuidCipherTexts);
|
||||||
return true;
|
return true;
|
||||||
} catch (InvalidGroupStateException | VerificationFailedException | IOException e) {
|
} catch (GroupChangeFailedException | GroupInsufficientRightsException | IOException | GroupNotAMemberException | GroupChangeBusyException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups.v2;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
|
import org.signal.zkgroup.VerificationFailedException;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
|
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class GroupCandidateHelper {
|
||||||
|
private final SignalServiceAccountManager signalServiceAccountManager;
|
||||||
|
private final RecipientDatabase recipientDatabase;
|
||||||
|
|
||||||
|
public GroupCandidateHelper(@NonNull Context context) {
|
||||||
|
signalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||||
|
recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(GroupCandidateHelper.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a recipient will create a {@link GroupCandidate} which may or may not have a profile key credential.
|
||||||
|
* <p>
|
||||||
|
* It will try to find missing profile key credentials from the server and persist locally.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
public @NonNull GroupCandidate recipientIdToCandidate(@NonNull RecipientId recipientId)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
final Recipient recipient = Recipient.resolved(recipientId);
|
||||||
|
|
||||||
|
UUID uuid = recipient.getUuid().orNull();
|
||||||
|
if (uuid == null) {
|
||||||
|
throw new AssertionError("Non UUID members should have need detected by now");
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<ProfileKeyCredential> profileKeyCredential = ProfileKeyUtil.profileKeyCredentialOptional(recipient.getProfileKeyCredential());
|
||||||
|
GroupCandidate candidate = new GroupCandidate(uuid, profileKeyCredential);
|
||||||
|
|
||||||
|
if (!candidate.hasProfileKeyCredential()) {
|
||||||
|
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
|
||||||
|
|
||||||
|
if (profileKey != null) {
|
||||||
|
try {
|
||||||
|
Optional<ProfileKeyCredential> profileKeyCredentialOptional = signalServiceAccountManager.resolveProfileKeyCredential(uuid, profileKey);
|
||||||
|
|
||||||
|
if (profileKeyCredentialOptional.isPresent()) {
|
||||||
|
candidate = candidate.withProfileKeyCredential(profileKeyCredentialOptional.get());
|
||||||
|
|
||||||
|
recipientDatabase.setProfileKeyCredential(recipient.getId(), profileKey, profileKeyCredentialOptional.get());
|
||||||
|
}
|
||||||
|
} catch (VerificationFailedException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
public @NonNull Set<GroupCandidate> recipientIdsToCandidates(@NonNull Collection<RecipientId> recipientIds)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
Set<GroupCandidate> result = new HashSet<>(recipientIds.size());
|
||||||
|
|
||||||
|
for (RecipientId recipientId : recipientIds) {
|
||||||
|
result.add(recipientIdToCandidate(recipientId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
||||||
import org.thoughtcrime.securesms.groups.GroupProtoUtil;
|
import org.thoughtcrime.securesms.groups.GroupProtoUtil;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupsV2Authorization;
|
||||||
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
|
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
|
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
|
||||||
@ -31,7 +32,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry;
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Authorization;
|
|
||||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||||
|
|
||||||
@ -217,7 +217,7 @@ public final class GroupsV2StateProcessor {
|
|||||||
.orNull();
|
.orNull();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
latestServerGroup = groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization);
|
latestServerGroup = groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
|
||||||
} catch (NotInGroupException e) {
|
} catch (NotInGroupException e) {
|
||||||
throw new GroupNotAMemberException(e);
|
throw new GroupNotAMemberException(e);
|
||||||
} catch (VerificationFailedException | InvalidGroupStateException e) {
|
} catch (VerificationFailedException | InvalidGroupStateException e) {
|
||||||
@ -238,7 +238,7 @@ public final class GroupsV2StateProcessor {
|
|||||||
|
|
||||||
private List<GroupLogEntry> getFullMemberHistory(@NonNull UUID selfUuid, int logsNeededFrom) throws IOException {
|
private List<GroupLogEntry> getFullMemberHistory(@NonNull UUID selfUuid, int logsNeededFrom) throws IOException {
|
||||||
try {
|
try {
|
||||||
Collection<DecryptedGroupHistoryEntry> groupStatesFromRevision = groupsV2Api.getGroupHistory(groupSecretParams, logsNeededFrom, groupsV2Authorization);
|
Collection<DecryptedGroupHistoryEntry> groupStatesFromRevision = groupsV2Api.getGroupHistory(groupSecretParams, logsNeededFrom, groupsV2Authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
|
||||||
ArrayList<GroupLogEntry> history = new ArrayList<>(groupStatesFromRevision.size());
|
ArrayList<GroupLogEntry> history = new ArrayList<>(groupStatesFromRevision.size());
|
||||||
|
|
||||||
for (DecryptedGroupHistoryEntry entry : groupStatesFromRevision) {
|
for (DecryptedGroupHistoryEntry entry : groupStatesFromRevision) {
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
package org.thoughtcrime.securesms.keyvalue;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
|
import org.signal.zkgroup.auth.AuthCredentialResponse;
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.TemporalAuthCredentialResponse;
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.TemporalAuthCredentialResponses;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupsV2Authorization;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Authorization.ValueCache {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(GroupsV2AuthorizationSignalStoreCache.class);
|
||||||
|
|
||||||
|
private static final String KEY = "gv2:auth_token_cache";
|
||||||
|
|
||||||
|
private final KeyValueStore store;
|
||||||
|
|
||||||
|
GroupsV2AuthorizationSignalStoreCache(KeyValueStore store) {
|
||||||
|
this.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear() {
|
||||||
|
store.beginWrite()
|
||||||
|
.remove(KEY)
|
||||||
|
.commit();
|
||||||
|
|
||||||
|
Log.i(TAG, "Cleared local response cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull Map<Integer, AuthCredentialResponse> read() {
|
||||||
|
byte[] credentialBlob = store.getBlob(KEY, null);
|
||||||
|
|
||||||
|
if (credentialBlob == null) {
|
||||||
|
Log.i(TAG, "No credentials responses are cached locally");
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
TemporalAuthCredentialResponses temporalCredentials = TemporalAuthCredentialResponses.parseFrom(credentialBlob);
|
||||||
|
HashMap<Integer, AuthCredentialResponse> result = new HashMap<>(temporalCredentials.getCredentialResponseCount());
|
||||||
|
|
||||||
|
for (TemporalAuthCredentialResponse credential : temporalCredentials.getCredentialResponseList()) {
|
||||||
|
result.put(credential.getDate(), new AuthCredentialResponse(credential.getAuthCredentialResponse().toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, String.format(Locale.US, "Loaded %d credentials from local storage", result.size()));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (InvalidProtocolBufferException | InvalidInputException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(@NonNull Map<Integer, AuthCredentialResponse> values) {
|
||||||
|
TemporalAuthCredentialResponses.Builder builder = TemporalAuthCredentialResponses.newBuilder();
|
||||||
|
|
||||||
|
for (Map.Entry<Integer, AuthCredentialResponse> entry : values.entrySet()) {
|
||||||
|
builder.addCredentialResponse(TemporalAuthCredentialResponse.newBuilder()
|
||||||
|
.setDate(entry.getKey())
|
||||||
|
.setAuthCredentialResponse(ByteString.copyFrom(entry.getValue().serialize())));
|
||||||
|
}
|
||||||
|
|
||||||
|
store.beginWrite()
|
||||||
|
.putBlob(KEY, builder.build().toByteArray())
|
||||||
|
.commit();
|
||||||
|
|
||||||
|
Log.i(TAG, String.format(Locale.US, "Written %d credentials to local storage", values.size()));
|
||||||
|
}
|
||||||
|
}
|
@ -40,6 +40,10 @@ public final class SignalStore {
|
|||||||
return new StorageServiceValues(getStore());
|
return new StorageServiceValues(getStore());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
|
||||||
|
return new GroupsV2AuthorizationSignalStoreCache(getStore());
|
||||||
|
}
|
||||||
|
|
||||||
public static long getLastPrekeyRefreshTime() {
|
public static long getLastPrekeyRefreshTime() {
|
||||||
return getStore().getLong(LAST_PREKEY_REFRESH_TIME, 0);
|
return getStore().getLong(LAST_PREKEY_REFRESH_TIME, 0);
|
||||||
}
|
}
|
||||||
|
@ -731,6 +731,10 @@ public class Recipient {
|
|||||||
return groupsV2Capability;
|
return groupsV2Capability;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Capability getUuidCapability() {
|
||||||
|
return uuidCapability;
|
||||||
|
}
|
||||||
|
|
||||||
public @Nullable byte[] getProfileKey() {
|
public @Nullable byte[] getProfileKey() {
|
||||||
return profileKey;
|
return profileKey;
|
||||||
}
|
}
|
||||||
|
@ -32,3 +32,12 @@ message DecryptedGroupV2Context {
|
|||||||
DecryptedGroupChange change = 2;
|
DecryptedGroupChange change = 2;
|
||||||
DecryptedGroup groupState = 3;
|
DecryptedGroup groupState = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message TemporalAuthCredentialResponse {
|
||||||
|
int32 date = 1;
|
||||||
|
bytes authCredentialResponse = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TemporalAuthCredentialResponses {
|
||||||
|
repeated TemporalAuthCredentialResponse credentialResponse = 1;
|
||||||
|
}
|
||||||
|
14
app/src/main/res/drawable/ic_add_members_circle_dark.xml
Normal file
14
app/src/main/res/drawable/ic_add_members_circle_dark.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="@color/core_grey_80" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item
|
||||||
|
android:bottom="12dp"
|
||||||
|
android:drawable="@drawable/ic_plus_24_ultramarine"
|
||||||
|
android:left="12dp"
|
||||||
|
android:right="12dp"
|
||||||
|
android:top="12dp" />
|
||||||
|
</layer-list>
|
14
app/src/main/res/drawable/ic_add_members_circle_light.xml
Normal file
14
app/src/main/res/drawable/ic_add_members_circle_light.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="@color/core_grey_02" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item
|
||||||
|
android:bottom="12dp"
|
||||||
|
android:drawable="@drawable/ic_plus_24_ultramarine"
|
||||||
|
android:left="12dp"
|
||||||
|
android:right="12dp"
|
||||||
|
android:top="12dp" />
|
||||||
|
</layer-list>
|
9
app/src/main/res/drawable/ic_plus_24_ultramarine.xml
Normal file
9
app/src/main/res/drawable/ic_plus_24_ultramarine.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/core_ultramarine"
|
||||||
|
android:pathData="M21,11.25l-8.25,0l0,-8.25l-1.5,0l0,8.25l-8.25,0l0,1.5l8.25,0l0,8.25l1.5,0l0,-8.25l8.25,0l0,-1.5z" />
|
||||||
|
</vector>
|
@ -160,7 +160,7 @@
|
|||||||
android:id="@+id/group_custom_notifications_controls"
|
android:id="@+id/group_custom_notifications_controls"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:constraint_referenced_ids="group_custom_notifications,group_custom_notifications_button"/>
|
app:constraint_referenced_ids="group_custom_notifications,group_custom_notifications_button" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
@ -268,6 +268,32 @@
|
|||||||
app:cardBackgroundColor="?android:attr/windowBackground"
|
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||||
app:layout_constraintTop_toBottomOf="@id/group_access_control_card">
|
app:layout_constraintTop_toBottomOf="@id/group_access_control_card">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/member_count_2"
|
||||||
|
style="@style/TextAppearance.Signal.Body2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
tools:text="12 members" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/add_members"
|
||||||
|
style="@style/Widget.Signal.Button.TextButton.Drawable.Ultramarine"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:drawableStart="?attr/manage_group_add_members_icon"
|
||||||
|
android:drawablePadding="8dp"
|
||||||
|
android:text="@string/ManageGroupActivity_add_members"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.groups.ui.GroupMemberListView
|
<org.thoughtcrime.securesms.groups.ui.GroupMemberListView
|
||||||
android:id="@+id/group_members"
|
android:id="@+id/group_members"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -275,6 +301,8 @@
|
|||||||
app:maxHeight="280dp"
|
app:maxHeight="280dp"
|
||||||
tools:listitem="@layout/group_recipient_list_item" />
|
tools:listitem="@layout/group_recipient_list_item" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
|
@ -227,6 +227,7 @@
|
|||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
<attr name="group_members_dialog_icon" format="reference"/>
|
<attr name="group_members_dialog_icon" format="reference"/>
|
||||||
|
<attr name="manage_group_add_members_icon" format="reference"/>
|
||||||
<attr name="lockscreen_watermark" format="reference" />
|
<attr name="lockscreen_watermark" format="reference" />
|
||||||
|
|
||||||
<attr name="recipient_preference_blocked" format="color"/>
|
<attr name="recipient_preference_blocked" format="color"/>
|
||||||
|
@ -499,8 +499,10 @@
|
|||||||
<string name="ManageGroupActivity_until_s">Until %1$s</string>
|
<string name="ManageGroupActivity_until_s">Until %1$s</string>
|
||||||
<string name="ManageGroupActivity_off">Off</string>
|
<string name="ManageGroupActivity_off">Off</string>
|
||||||
<string name="ManageGroupActivity_on">On</string>
|
<string name="ManageGroupActivity_on">On</string>
|
||||||
|
<string name="ManageGroupActivity_add_members">Add members</string>
|
||||||
|
|
||||||
<string name="ManageGroupActivity_you_dont_have_the_rights_to_do_this">You don\'t have the rights to do this</string>
|
<string name="ManageGroupActivity_you_dont_have_the_rights_to_do_this">You don\'t have the rights to do this</string>
|
||||||
|
<string name="ManageGroupActivity_not_capable">Someone you added does not support new groups and needs to update Signal</string>
|
||||||
<string name="ManageGroupActivity_failed_to_update_the_group">Failed to update the group</string>
|
<string name="ManageGroupActivity_failed_to_update_the_group">Failed to update the group</string>
|
||||||
<string name="ManageGroupActivity_youre_not_a_member_of_the_group">You\'re not a member of the group</string>
|
<string name="ManageGroupActivity_youre_not_a_member_of_the_group">You\'re not a member of the group</string>
|
||||||
|
|
||||||
|
@ -432,6 +432,10 @@
|
|||||||
<item name="android:textColor">@color/ultramarine_text_button</item>
|
<item name="android:textColor">@color/ultramarine_text_button</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Signal.Button.TextButton.Drawable.Ultramarine" >
|
||||||
|
<item name="android:textColor">@color/ultramarine_text_button</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Signal.Button.CalleeDialog" parent="Widget.AppCompat.Button">
|
<style name="Widget.Signal.Button.CalleeDialog" parent="Widget.AppCompat.Button">
|
||||||
<item name="android:textColor">@color/core_ultramarine</item>
|
<item name="android:textColor">@color/core_ultramarine</item>
|
||||||
<item name="android:background">@drawable/callee_dialog_button_background</item>
|
<item name="android:background">@drawable/callee_dialog_button_background</item>
|
||||||
|
@ -405,6 +405,7 @@
|
|||||||
<item name="quote_dismiss_button_tint">@color/core_grey_70</item>
|
<item name="quote_dismiss_button_tint">@color/core_grey_70</item>
|
||||||
|
|
||||||
<item name="group_members_dialog_icon">@drawable/ic_group_outline_24</item>
|
<item name="group_members_dialog_icon">@drawable/ic_group_outline_24</item>
|
||||||
|
<item name="manage_group_add_members_icon">@drawable/ic_add_members_circle_light</item>
|
||||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
|
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
|
||||||
|
|
||||||
<item name="search_toolbar_background">@color/white</item>
|
<item name="search_toolbar_background">@color/white</item>
|
||||||
@ -689,6 +690,7 @@
|
|||||||
<item name="quote_dismiss_button_tint">@color/core_white</item>
|
<item name="quote_dismiss_button_tint">@color/core_white</item>
|
||||||
|
|
||||||
<item name="group_members_dialog_icon">@drawable/ic_group_solid_24</item>
|
<item name="group_members_dialog_icon">@drawable/ic_group_solid_24</item>
|
||||||
|
<item name="manage_group_add_members_icon">@drawable/ic_add_members_circle_dark</item>
|
||||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
|
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
|
||||||
<item name="search_toolbar_background">@color/core_grey_95</item>
|
<item name="search_toolbar_background">@color/core_grey_95</item>
|
||||||
<item name="search_background">@color/black</item>
|
<item name="search_background">@color/black</item>
|
||||||
|
@ -26,9 +26,7 @@ import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
|||||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
|
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Authorization;
|
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
|
||||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||||
@ -758,8 +756,4 @@ public class SignalServiceAccountManager {
|
|||||||
public GroupsV2Api getGroupsV2Api() {
|
public GroupsV2Api getGroupsV2Api() {
|
||||||
return new GroupsV2Api(pushServiceSocket, groupsV2Operations);
|
return new GroupsV2Api(pushServiceSocket, groupsV2Operations);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupsV2Authorization createGroupsV2Authorization(UUID self) {
|
|
||||||
return new GroupsV2Authorization(self, pushServiceSocket, groupsV2Operations.getAuthOperations());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -39,8 +39,8 @@ public final class GroupCandidate {
|
|||||||
return profileKeyCredential.isPresent();
|
return profileKeyCredential.isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupCandidate withProfileKeyCredential(Optional<ProfileKeyCredential> profileKeyCredential) {
|
public GroupCandidate withProfileKeyCredential(ProfileKeyCredential profileKeyCredential) {
|
||||||
return new GroupCandidate(uuid, profileKeyCredential);
|
return new GroupCandidate(uuid, Optional.of(profileKeyCredential));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<UUID> toUuidList(Collection<GroupCandidate> candidates) {
|
public static List<UUID> toUuidList(Collection<GroupCandidate> candidates) {
|
||||||
|
@ -9,14 +9,22 @@ import org.signal.storageservice.protos.groups.GroupChange;
|
|||||||
import org.signal.storageservice.protos.groups.GroupChanges;
|
import org.signal.storageservice.protos.groups.GroupChanges;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||||
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
import org.signal.zkgroup.VerificationFailedException;
|
import org.signal.zkgroup.VerificationFailedException;
|
||||||
|
import org.signal.zkgroup.auth.AuthCredential;
|
||||||
|
import org.signal.zkgroup.auth.AuthCredentialPresentation;
|
||||||
|
import org.signal.zkgroup.auth.AuthCredentialResponse;
|
||||||
|
import org.signal.zkgroup.auth.ClientZkAuthOperations;
|
||||||
import org.signal.zkgroup.groups.ClientZkGroupCipher;
|
import org.signal.zkgroup.groups.ClientZkGroupCipher;
|
||||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public final class GroupsV2Api {
|
public final class GroupsV2Api {
|
||||||
|
|
||||||
@ -28,9 +36,34 @@ public final class GroupsV2Api {
|
|||||||
this.groupsOperations = groupsOperations;
|
this.groupsOperations = groupsOperations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides 7 days of credentials, which you should cache.
|
||||||
|
*/
|
||||||
|
public HashMap<Integer, AuthCredentialResponse> getCredentials(int today)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
return parseCredentialResponse(socket.retrieveGroupsV2Credentials(today));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an auth token from a credential response.
|
||||||
|
*/
|
||||||
|
public GroupsV2AuthorizationString getGroupsV2AuthorizationString(UUID self,
|
||||||
|
int today,
|
||||||
|
GroupSecretParams groupSecretParams,
|
||||||
|
AuthCredentialResponse authCredentialResponse)
|
||||||
|
throws VerificationFailedException
|
||||||
|
{
|
||||||
|
ClientZkAuthOperations authOperations = groupsOperations.getAuthOperations();
|
||||||
|
AuthCredential authCredential = authOperations.receiveAuthCredential(self, today, authCredentialResponse);
|
||||||
|
AuthCredentialPresentation authCredentialPresentation = authOperations.createAuthCredentialPresentation(new SecureRandom(), groupSecretParams, authCredential);
|
||||||
|
|
||||||
|
return new GroupsV2AuthorizationString(groupSecretParams, authCredentialPresentation);
|
||||||
|
}
|
||||||
|
|
||||||
public void putNewGroup(GroupsV2Operations.NewGroup newGroup,
|
public void putNewGroup(GroupsV2Operations.NewGroup newGroup,
|
||||||
GroupsV2Authorization authorization)
|
GroupsV2AuthorizationString authorization)
|
||||||
throws IOException, VerificationFailedException
|
throws IOException
|
||||||
{
|
{
|
||||||
Group group = newGroup.getNewGroupMessage();
|
Group group = newGroup.getNewGroupMessage();
|
||||||
|
|
||||||
@ -42,14 +75,14 @@ public final class GroupsV2Api {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.putNewGroupsV2Group(group, authorization.getAuthorizationForToday(newGroup.getGroupSecretParams()));
|
socket.putNewGroupsV2Group(group, authorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DecryptedGroup getGroup(GroupSecretParams groupSecretParams,
|
public DecryptedGroup getGroup(GroupSecretParams groupSecretParams,
|
||||||
GroupsV2Authorization authorization)
|
GroupsV2AuthorizationString authorization)
|
||||||
throws IOException, InvalidGroupStateException, VerificationFailedException
|
throws IOException, InvalidGroupStateException, VerificationFailedException
|
||||||
{
|
{
|
||||||
Group group = socket.getGroupsV2Group(authorization.getAuthorizationForToday(groupSecretParams));
|
Group group = socket.getGroupsV2Group(authorization);
|
||||||
|
|
||||||
return groupsOperations.forGroup(groupSecretParams)
|
return groupsOperations.forGroup(groupSecretParams)
|
||||||
.decryptGroup(group);
|
.decryptGroup(group);
|
||||||
@ -57,10 +90,10 @@ public final class GroupsV2Api {
|
|||||||
|
|
||||||
public List<DecryptedGroupHistoryEntry> getGroupHistory(GroupSecretParams groupSecretParams,
|
public List<DecryptedGroupHistoryEntry> getGroupHistory(GroupSecretParams groupSecretParams,
|
||||||
int fromRevision,
|
int fromRevision,
|
||||||
GroupsV2Authorization authorization)
|
GroupsV2AuthorizationString authorization)
|
||||||
throws IOException, InvalidGroupStateException, VerificationFailedException
|
throws IOException, InvalidGroupStateException, VerificationFailedException
|
||||||
{
|
{
|
||||||
GroupChanges group = socket.getGroupsV2GroupHistory(fromRevision, authorization.getAuthorizationForToday(groupSecretParams));
|
GroupChanges group = socket.getGroupsV2GroupHistory(fromRevision, authorization);
|
||||||
|
|
||||||
List<GroupChanges.GroupChangeState> changesList = group.getGroupChangesList();
|
List<GroupChanges.GroupChangeState> changesList = group.getGroupChangesList();
|
||||||
ArrayList<DecryptedGroupHistoryEntry> result = new ArrayList<>(changesList.size());
|
ArrayList<DecryptedGroupHistoryEntry> result = new ArrayList<>(changesList.size());
|
||||||
@ -82,10 +115,10 @@ public final class GroupsV2Api {
|
|||||||
|
|
||||||
public String uploadAvatar(byte[] avatar,
|
public String uploadAvatar(byte[] avatar,
|
||||||
GroupSecretParams groupSecretParams,
|
GroupSecretParams groupSecretParams,
|
||||||
GroupsV2Authorization authorization)
|
GroupsV2AuthorizationString authorization)
|
||||||
throws IOException, VerificationFailedException
|
throws IOException
|
||||||
{
|
{
|
||||||
AvatarUploadAttributes form = socket.getGroupsV2AvatarUploadForm(authorization.getAuthorizationForToday(groupSecretParams));
|
AvatarUploadAttributes form = socket.getGroupsV2AvatarUploadForm(authorization.toString());
|
||||||
|
|
||||||
byte[] cipherText;
|
byte[] cipherText;
|
||||||
try {
|
try {
|
||||||
@ -101,13 +134,31 @@ public final class GroupsV2Api {
|
|||||||
|
|
||||||
public DecryptedGroupChange patchGroup(GroupChange.Actions groupChange,
|
public DecryptedGroupChange patchGroup(GroupChange.Actions groupChange,
|
||||||
GroupSecretParams groupSecretParams,
|
GroupSecretParams groupSecretParams,
|
||||||
GroupsV2Authorization authorization)
|
GroupsV2AuthorizationString authorization)
|
||||||
throws IOException, VerificationFailedException, InvalidGroupStateException
|
throws IOException, VerificationFailedException, InvalidGroupStateException
|
||||||
{
|
{
|
||||||
String authorizationBasic = authorization.getAuthorizationForToday(groupSecretParams);
|
GroupChange groupChanges = socket.patchGroupsV2Group(groupChange, authorization.toString());
|
||||||
GroupChange groupChanges = socket.patchGroupsV2Group(groupChange, authorizationBasic);
|
|
||||||
|
|
||||||
return groupsOperations.forGroup(groupSecretParams)
|
return groupsOperations.forGroup(groupSecretParams)
|
||||||
.decryptChange(groupChanges, true);
|
.decryptChange(groupChanges, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static HashMap<Integer, AuthCredentialResponse> parseCredentialResponse(CredentialResponse credentialResponse)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
HashMap<Integer, AuthCredentialResponse> credentials = new HashMap<>();
|
||||||
|
|
||||||
|
for (TemporalCredential credential : credentialResponse.getCredentials()) {
|
||||||
|
AuthCredentialResponse authCredentialResponse;
|
||||||
|
try {
|
||||||
|
authCredentialResponse = new AuthCredentialResponse(credential.getCredential());
|
||||||
|
} catch (InvalidInputException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials.put(credential.getRedemptionTime(), authCredentialResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return credentials;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,146 +0,0 @@
|
|||||||
package org.whispersystems.signalservice.api.groupsv2;
|
|
||||||
|
|
||||||
import org.signal.zkgroup.InvalidInputException;
|
|
||||||
import org.signal.zkgroup.VerificationFailedException;
|
|
||||||
import org.signal.zkgroup.auth.AuthCredential;
|
|
||||||
import org.signal.zkgroup.auth.AuthCredentialPresentation;
|
|
||||||
import org.signal.zkgroup.auth.AuthCredentialResponse;
|
|
||||||
import org.signal.zkgroup.auth.ClientZkAuthOperations;
|
|
||||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
|
||||||
import org.whispersystems.libsignal.logging.Log;
|
|
||||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
|
||||||
import org.whispersystems.signalservice.internal.util.Hex;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import okhttp3.Credentials;
|
|
||||||
|
|
||||||
public final class GroupsV2Authorization {
|
|
||||||
|
|
||||||
private static final String TAG = GroupsV2Authorization.class.getSimpleName();
|
|
||||||
|
|
||||||
private final UUID self;
|
|
||||||
private final PushServiceSocket socket;
|
|
||||||
private final ClientZkAuthOperations authOperations;
|
|
||||||
private AuthorizationFactory currentFactory;
|
|
||||||
|
|
||||||
public GroupsV2Authorization(UUID self, PushServiceSocket socket, ClientZkAuthOperations authOperations) {
|
|
||||||
this.self = self;
|
|
||||||
this.socket = socket;
|
|
||||||
this.authOperations = authOperations;
|
|
||||||
}
|
|
||||||
|
|
||||||
String getAuthorizationForToday(GroupSecretParams groupSecretParams)
|
|
||||||
throws IOException, VerificationFailedException
|
|
||||||
{
|
|
||||||
final int today = AuthorizationFactory.currentTimeDays();
|
|
||||||
|
|
||||||
final AuthorizationFactory currentFactory = getCurrentFactory();
|
|
||||||
if (currentFactory != null) {
|
|
||||||
try {
|
|
||||||
return currentFactory.getAuthorization(groupSecretParams, today);
|
|
||||||
} catch (NoCredentialForRedemptionTimeException e) {
|
|
||||||
Log.i(TAG, "Auth out of date, will update auth and try again");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "Getting new auth tokens");
|
|
||||||
setCurrentFactory(createFactory(socket.retrieveGroupsV2Credentials(today)));
|
|
||||||
|
|
||||||
try {
|
|
||||||
return getCurrentFactory().getAuthorization(groupSecretParams, today);
|
|
||||||
} catch (NoCredentialForRedemptionTimeException e) {
|
|
||||||
Log.w(TAG, "The credentials returned did not include the day requested");
|
|
||||||
throw new IOException("Failed to get credentials");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private AuthorizationFactory createFactory(CredentialResponse credentialResponse)
|
|
||||||
throws IOException, VerificationFailedException
|
|
||||||
{
|
|
||||||
HashMap<Integer, AuthCredentialResponse> credentials = new HashMap<>();
|
|
||||||
|
|
||||||
for (TemporalCredential credential : credentialResponse.getCredentials()) {
|
|
||||||
AuthCredentialResponse authCredentialResponse;
|
|
||||||
try {
|
|
||||||
authCredentialResponse = new AuthCredentialResponse(credential.getCredential());
|
|
||||||
} catch (InvalidInputException e) {
|
|
||||||
throw new IOException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
credentials.put(credential.getRedemptionTime(), authCredentialResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AuthorizationFactory(self, authOperations, credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized AuthorizationFactory getCurrentFactory() {
|
|
||||||
return currentFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void setCurrentFactory(AuthorizationFactory currentFactory) {
|
|
||||||
this.currentFactory = currentFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class AuthorizationFactory {
|
|
||||||
|
|
||||||
private final SecureRandom random;
|
|
||||||
private final ClientZkAuthOperations clientZkAuthOperations;
|
|
||||||
private final Map<Integer, AuthCredential> credentials;
|
|
||||||
|
|
||||||
AuthorizationFactory(UUID self,
|
|
||||||
ClientZkAuthOperations clientZkAuthOperations,
|
|
||||||
Map<Integer, AuthCredentialResponse> credentialResponseMap)
|
|
||||||
throws VerificationFailedException
|
|
||||||
{
|
|
||||||
this.random = new SecureRandom();
|
|
||||||
this.clientZkAuthOperations = clientZkAuthOperations;
|
|
||||||
this.credentials = verifyCredentials(self, clientZkAuthOperations, credentialResponseMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int currentTimeDays() {
|
|
||||||
return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
|
|
||||||
String getAuthorization(GroupSecretParams groupSecretParams, int redemptionTime)
|
|
||||||
throws NoCredentialForRedemptionTimeException
|
|
||||||
{
|
|
||||||
AuthCredential authCredential = credentials.get(redemptionTime);
|
|
||||||
|
|
||||||
if (authCredential == null) {
|
|
||||||
throw new NoCredentialForRedemptionTimeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthCredentialPresentation authCredentialPresentation = clientZkAuthOperations.createAuthCredentialPresentation(random, groupSecretParams, authCredential);
|
|
||||||
|
|
||||||
String username = Hex.toStringCondensed(groupSecretParams.getPublicParams().serialize());
|
|
||||||
String password = Hex.toStringCondensed(authCredentialPresentation.serialize());
|
|
||||||
|
|
||||||
return Credentials.basic(username, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<Integer, AuthCredential> verifyCredentials(UUID self,
|
|
||||||
ClientZkAuthOperations clientZkAuthOperations,
|
|
||||||
Map<Integer, AuthCredentialResponse> credentialResponseMap)
|
|
||||||
throws VerificationFailedException
|
|
||||||
{
|
|
||||||
Map<Integer, AuthCredential> credentials = new HashMap<>(credentialResponseMap.size());
|
|
||||||
|
|
||||||
for (Map.Entry<Integer, AuthCredentialResponse> entry : credentialResponseMap.entrySet()) {
|
|
||||||
int redemptionTime = entry.getKey();
|
|
||||||
AuthCredentialResponse authCredentialResponse = entry.getValue();
|
|
||||||
|
|
||||||
AuthCredential authCredential = clientZkAuthOperations.receiveAuthCredential(self, redemptionTime, authCredentialResponse);
|
|
||||||
|
|
||||||
credentials.put(redemptionTime, authCredential);
|
|
||||||
}
|
|
||||||
|
|
||||||
return credentials;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,24 @@
|
|||||||
|
package org.whispersystems.signalservice.api.groupsv2;
|
||||||
|
|
||||||
|
import org.signal.zkgroup.auth.AuthCredentialPresentation;
|
||||||
|
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||||
|
import org.whispersystems.signalservice.internal.util.Hex;
|
||||||
|
|
||||||
|
import okhttp3.Credentials;
|
||||||
|
|
||||||
|
public final class GroupsV2AuthorizationString {
|
||||||
|
|
||||||
|
private final String authString;
|
||||||
|
|
||||||
|
GroupsV2AuthorizationString(GroupSecretParams groupSecretParams, AuthCredentialPresentation authCredentialPresentation) {
|
||||||
|
String username = Hex.toStringCondensed(groupSecretParams.getPublicParams().serialize());
|
||||||
|
String password = Hex.toStringCondensed(authCredentialPresentation.serialize());
|
||||||
|
|
||||||
|
authString = Credentials.basic(username, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return authString;
|
||||||
|
}
|
||||||
|
}
|
@ -170,6 +170,28 @@ public final class GroupsV2Operations {
|
|||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GroupChange.Actions.Builder createModifyGroupMembershipChange(Set<GroupCandidate> membersToAdd, UUID selfUuid) {
|
||||||
|
final GroupOperations groupOperations = forGroup(groupSecretParams);
|
||||||
|
|
||||||
|
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
|
||||||
|
|
||||||
|
for (GroupCandidate credential : membersToAdd) {
|
||||||
|
Member.Role newMemberRole = Member.Role.DEFAULT;
|
||||||
|
ProfileKeyCredential profileKeyCredential = credential.getProfileKeyCredential().orNull();
|
||||||
|
|
||||||
|
if (profileKeyCredential != null) {
|
||||||
|
actions.addAddMembers(GroupChange.Actions.AddMemberAction.newBuilder()
|
||||||
|
.setAdded(groupOperations.member(profileKeyCredential, newMemberRole)));
|
||||||
|
} else {
|
||||||
|
actions.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction.newBuilder()
|
||||||
|
.setAdded(groupOperations.invitee(credential.getUuid(), newMemberRole)
|
||||||
|
.setAddedByUserId(encryptUuid(selfUuid))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
public GroupChange.Actions.Builder createModifyGroupTimerChange(int timerDurationSeconds) {
|
public GroupChange.Actions.Builder createModifyGroupTimerChange(int timerDurationSeconds) {
|
||||||
return GroupChange.Actions
|
return GroupChange.Actions
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
@ -233,7 +255,11 @@ public final class GroupsV2Operations {
|
|||||||
List<DecryptedPendingMember> decryptedPendingMembers = new ArrayList<>(pendingMembersList.size());
|
List<DecryptedPendingMember> decryptedPendingMembers = new ArrayList<>(pendingMembersList.size());
|
||||||
|
|
||||||
for (Member member : membersList) {
|
for (Member member : membersList) {
|
||||||
decryptedMembers.add(decryptMember(member));
|
try {
|
||||||
|
decryptedMembers.add(decryptMember(member).build());
|
||||||
|
} catch (InvalidInputException e) {
|
||||||
|
throw new InvalidGroupStateException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (PendingMember member : pendingMembersList) {
|
for (PendingMember member : pendingMembersList) {
|
||||||
@ -289,12 +315,11 @@ public final class GroupsV2Operations {
|
|||||||
|
|
||||||
// Field 3
|
// Field 3
|
||||||
for (GroupChange.Actions.AddMemberAction addMemberAction : actions.getAddMembersList()) {
|
for (GroupChange.Actions.AddMemberAction addMemberAction : actions.getAddMembersList()) {
|
||||||
UUID uuid = decryptUuid(addMemberAction.getAdded().getUserId());
|
try {
|
||||||
builder.addNewMembers(DecryptedMember.newBuilder()
|
builder.addNewMembers(decryptMember(addMemberAction.getAdded()).setJoinedAtVersion(actions.getVersion()));
|
||||||
.setJoinedAtVersion(actions.getVersion())
|
} catch (InvalidInputException e) {
|
||||||
.setRole(addMemberAction.getAdded().getRole())
|
throw new InvalidGroupStateException(e);
|
||||||
.setUuid(UuidUtil.toByteString(uuid))
|
}
|
||||||
.setProfileKey(decryptProfileKeyToByteString(addMemberAction.getAdded().getProfileKey(), uuid)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Field 4
|
// Field 4
|
||||||
@ -327,7 +352,7 @@ public final class GroupsV2Operations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Field 7
|
// Field 7
|
||||||
for (GroupChange.Actions.AddPendingMemberAction addPendingMemberAction:actions.getAddPendingMembersList()) {
|
for (GroupChange.Actions.AddPendingMemberAction addPendingMemberAction : actions.getAddPendingMembersList()) {
|
||||||
PendingMember added = addPendingMemberAction.getAdded();
|
PendingMember added = addPendingMemberAction.getAdded();
|
||||||
Member member = added.getMember();
|
Member member = added.getMember();
|
||||||
ByteString uuidCipherText = member.getUserId();
|
ByteString uuidCipherText = member.getUserId();
|
||||||
@ -397,18 +422,28 @@ public final class GroupsV2Operations {
|
|||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private DecryptedMember decryptMember(Member member)
|
private DecryptedMember.Builder decryptMember(Member member)
|
||||||
throws InvalidGroupStateException, VerificationFailedException
|
throws InvalidGroupStateException, VerificationFailedException, InvalidInputException
|
||||||
{
|
{
|
||||||
ByteString userId = member.getUserId();
|
if (member.getPresentation().isEmpty()) {
|
||||||
UUID uuid = decryptUuid(userId);
|
UUID uuid = decryptUuid(member.getUserId());
|
||||||
|
|
||||||
return DecryptedMember.newBuilder()
|
return DecryptedMember.newBuilder()
|
||||||
.setUuid(UuidUtil.toByteString(uuid))
|
.setUuid(UuidUtil.toByteString(uuid))
|
||||||
.setJoinedAtVersion(member.getJoinedAtVersion())
|
.setJoinedAtVersion(member.getJoinedAtVersion())
|
||||||
.setProfileKey(decryptProfileKeyToByteString(member.getProfileKey(), uuid))
|
.setProfileKey(decryptProfileKeyToByteString(member.getProfileKey(), uuid))
|
||||||
.setRole(member.getRole())
|
.setRole(member.getRole());
|
||||||
.build();
|
} else {
|
||||||
|
ProfileKeyCredentialPresentation profileKeyCredentialPresentation = new ProfileKeyCredentialPresentation(member.getPresentation().toByteArray());
|
||||||
|
UUID uuid = clientZkGroupCipher.decryptUuid(profileKeyCredentialPresentation.getUuidCiphertext());
|
||||||
|
ProfileKey profileKey = clientZkGroupCipher.decryptProfileKey(profileKeyCredentialPresentation.getProfileKeyCiphertext(), uuid);
|
||||||
|
|
||||||
|
return DecryptedMember.newBuilder()
|
||||||
|
.setUuid(UuidUtil.toByteString(uuid))
|
||||||
|
.setJoinedAtVersion(member.getJoinedAtVersion())
|
||||||
|
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
|
||||||
|
.setRole(member.getRole());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DecryptedPendingMember decryptMember(PendingMember member)
|
private DecryptedPendingMember decryptMember(PendingMember member)
|
||||||
|
@ -34,6 +34,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||||||
import org.whispersystems.signalservice.FeatureFlags;
|
import org.whispersystems.signalservice.FeatureFlags;
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.CredentialResponse;
|
import org.whispersystems.signalservice.api.groupsv2.CredentialResponse;
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
||||||
@ -72,6 +73,7 @@ import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResp
|
|||||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
|
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
|
||||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
|
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
|
||||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||||
|
import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException;
|
||||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||||
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
|
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
|
||||||
@ -1836,26 +1838,29 @@ public class PushServiceSocket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final ResponseCodeHandler GROUPS_V2_PUT_RESPONSE_HANDLER = NO_HANDLER;
|
private static final ResponseCodeHandler GROUPS_V2_PUT_RESPONSE_HANDLER = NO_HANDLER;
|
||||||
private static final ResponseCodeHandler GROUPS_V2_PATCH_RESPONSE_HANDLER = NO_HANDLER;
|
|
||||||
private static final ResponseCodeHandler GROUPS_V2_GET_LOGS_HANDLER = NO_HANDLER;
|
private static final ResponseCodeHandler GROUPS_V2_GET_LOGS_HANDLER = NO_HANDLER;
|
||||||
private static final ResponseCodeHandler GROUPS_V2_GET_CURRENT_HANDLER = responseCode -> {
|
private static final ResponseCodeHandler GROUPS_V2_GET_CURRENT_HANDLER = responseCode -> {
|
||||||
if (responseCode == 403) throw new NotInGroupException();
|
if (responseCode == 403) throw new NotInGroupException();
|
||||||
};
|
};
|
||||||
|
private static final ResponseCodeHandler GROUPS_V2_PATCH_RESPONSE_HANDLER = responseCode -> {
|
||||||
|
if (responseCode == 400) throw new GroupPatchNotAcceptedException();
|
||||||
|
if (responseCode == 403) throw new NotInGroupException();
|
||||||
|
};
|
||||||
|
|
||||||
public void putNewGroupsV2Group(Group group, String authorization)
|
public void putNewGroupsV2Group(Group group, GroupsV2AuthorizationString authorization)
|
||||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||||
{
|
{
|
||||||
makeStorageRequest(authorization,
|
makeStorageRequest(authorization.toString(),
|
||||||
GROUPSV2_GROUP,
|
GROUPSV2_GROUP,
|
||||||
"PUT",
|
"PUT",
|
||||||
protobufRequestBody(group),
|
protobufRequestBody(group),
|
||||||
GROUPS_V2_PUT_RESPONSE_HANDLER);
|
GROUPS_V2_PUT_RESPONSE_HANDLER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Group getGroupsV2Group(String authorization)
|
public Group getGroupsV2Group(GroupsV2AuthorizationString authorization)
|
||||||
throws NonSuccessfulResponseCodeException, PushNetworkException, InvalidProtocolBufferException
|
throws NonSuccessfulResponseCodeException, PushNetworkException, InvalidProtocolBufferException
|
||||||
{
|
{
|
||||||
ResponseBody response = makeStorageRequest(authorization,
|
ResponseBody response = makeStorageRequest(authorization.toString(),
|
||||||
GROUPSV2_GROUP,
|
GROUPSV2_GROUP,
|
||||||
"GET",
|
"GET",
|
||||||
null,
|
null,
|
||||||
@ -1888,10 +1893,10 @@ public class PushServiceSocket {
|
|||||||
return GroupChange.parseFrom(readBodyBytes(response));
|
return GroupChange.parseFrom(readBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupChanges getGroupsV2GroupHistory(int fromVersion, String authorization)
|
public GroupChanges getGroupsV2GroupHistory(int fromVersion, GroupsV2AuthorizationString authorization)
|
||||||
throws NonSuccessfulResponseCodeException, PushNetworkException, InvalidProtocolBufferException
|
throws NonSuccessfulResponseCodeException, PushNetworkException, InvalidProtocolBufferException
|
||||||
{
|
{
|
||||||
ResponseBody response = makeStorageRequest(authorization,
|
ResponseBody response = makeStorageRequest(authorization.toString(),
|
||||||
String.format(Locale.US, GROUPSV2_GROUP_CHANGES, fromVersion),
|
String.format(Locale.US, GROUPSV2_GROUP_CHANGES, fromVersion),
|
||||||
"GET",
|
"GET",
|
||||||
null,
|
null,
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
package org.whispersystems.signalservice.internal.push.exceptions;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||||
|
|
||||||
|
public final class GroupPatchNotAcceptedException extends NonSuccessfulResponseCodeException {
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user