mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
Add additional Group Calling features.
This commit is contained in:
@@ -23,7 +23,6 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList;
|
||||
import org.thoughtcrime.securesms.insights.InsightsConstants;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
@@ -50,6 +49,7 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class MessageDatabase extends Database implements MmsSmsColumns {
|
||||
|
||||
@@ -130,6 +130,12 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
public abstract @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer);
|
||||
public abstract @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer);
|
||||
public abstract @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer);
|
||||
public abstract @NonNull void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId,
|
||||
@NonNull RecipientId sender,
|
||||
long timestamp,
|
||||
@Nullable String messageGroupCallEraId,
|
||||
@Nullable String peekGroupCallEraId,
|
||||
@NonNull Collection<UUID> peekJoinedUuids);
|
||||
|
||||
public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type);
|
||||
public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message);
|
||||
|
||||
@@ -92,6 +92,7 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.Avatar;
|
||||
|
||||
@@ -397,6 +398,17 @@ public class MmsDatabase extends MessageDatabase {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId,
|
||||
@NonNull RecipientId sender,
|
||||
long timestamp,
|
||||
@Nullable String messageGroupCallEraId,
|
||||
@Nullable String peekGroupCallEraId,
|
||||
@NonNull Collection<UUID> peekJoinedUuids)
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type) {
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
@@ -44,6 +44,7 @@ public interface MmsSmsColumns {
|
||||
protected static final long GV1_MIGRATION_TYPE = 9;
|
||||
protected static final long INCOMING_VIDEO_CALL_TYPE = 10;
|
||||
protected static final long OUTGOING_VIDEO_CALL_TYPE = 11;
|
||||
protected static final long GROUP_CALL_TYPE = 12;
|
||||
|
||||
protected static final long BASE_INBOX_TYPE = 20;
|
||||
protected static final long BASE_OUTBOX_TYPE = 21;
|
||||
@@ -214,7 +215,8 @@ public interface MmsSmsColumns {
|
||||
isOutgoingAudioCall(type) ||
|
||||
isOutgoingVideoCall(type) ||
|
||||
isMissedAudioCall(type) ||
|
||||
isMissedVideoCall(type);
|
||||
isMissedVideoCall(type) ||
|
||||
isGroupCall(type);
|
||||
}
|
||||
|
||||
public static boolean isExpirationTimerUpdate(long type) {
|
||||
@@ -237,7 +239,6 @@ public interface MmsSmsColumns {
|
||||
return type == OUTGOING_VIDEO_CALL_TYPE;
|
||||
}
|
||||
|
||||
|
||||
public static boolean isMissedAudioCall(long type) {
|
||||
return type == MISSED_AUDIO_CALL_TYPE;
|
||||
}
|
||||
@@ -246,6 +247,10 @@ public interface MmsSmsColumns {
|
||||
return type == MISSED_VIDEO_CALL_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isGroupCall(long type) {
|
||||
return type == GROUP_CALL_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isGroupUpdate(long type) {
|
||||
return (type & GROUP_UPDATE_BIT) != 0;
|
||||
}
|
||||
|
||||
@@ -35,9 +35,11 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
@@ -57,6 +59,7 @@ import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
@@ -69,7 +72,9 @@ import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Database for storage of SMS messages.
|
||||
@@ -666,6 +671,89 @@ public class SmsDatabase extends MessageDatabase {
|
||||
return insertCallLog(address, isVideoOffer ? Types.MISSED_VIDEO_CALL_TYPE : Types.MISSED_AUDIO_CALL_TYPE, true, timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId,
|
||||
@NonNull RecipientId sender,
|
||||
long timestamp,
|
||||
@Nullable String messageGroupCallEraId,
|
||||
@Nullable String peekGroupCallEraId,
|
||||
@NonNull Collection<UUID> peekJoinedUuids)
|
||||
{
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
Recipient recipient = Recipient.resolved(groupRecipientId);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
|
||||
boolean peerEraIdSameAsPrevious = updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids);
|
||||
|
||||
if (!peerEraIdSameAsPrevious && !Util.isEmpty(peekGroupCallEraId)) {
|
||||
byte[] updateDetails = GroupCallUpdateDetails.newBuilder()
|
||||
.setEraId(Util.emptyIfNull(peekGroupCallEraId))
|
||||
.setStartedCallUuid(Recipient.resolved(sender).requireUuid().toString())
|
||||
.setStartedCallTimestamp(timestamp)
|
||||
.addAllInCallUuids(Stream.of(peekJoinedUuids).map(UUID::toString).toList())
|
||||
.build()
|
||||
.toByteArray();
|
||||
|
||||
String body = Base64.encodeBytes(updateDetails);
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(RECIPIENT_ID, sender.serialize());
|
||||
values.put(ADDRESS_DEVICE_ID, 1);
|
||||
values.put(DATE_RECEIVED, timestamp);
|
||||
values.put(DATE_SENT, timestamp);
|
||||
values.put(READ, 0);
|
||||
values.put(BODY, body);
|
||||
values.put(TYPE, Types.GROUP_CALL_TYPE);
|
||||
values.put(THREAD_ID, threadId);
|
||||
|
||||
db.insert(TABLE_NAME, null, values);
|
||||
|
||||
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
|
||||
}
|
||||
|
||||
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
|
||||
|
||||
notifyConversationListeners(threadId);
|
||||
ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId));
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean updatePreviousGroupCall(long threadId, @Nullable String peekGroupCallEraId, @NonNull Collection<UUID> peekJoinedUuids) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String where = TYPE + " = ? AND " + THREAD_ID + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(Types.GROUP_CALL_TYPE, threadId);
|
||||
|
||||
try (Reader reader = new Reader(db.query(TABLE_NAME, MESSAGE_PROJECTION, where, args, null, null, DATE_RECEIVED + " DESC", "1"))) {
|
||||
MessageRecord record = reader.getNext();
|
||||
if (record == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.getBody());
|
||||
boolean sameEraId = groupCallUpdateDetails.getEraId().equals(peekGroupCallEraId) && !Util.isEmpty(peekGroupCallEraId);
|
||||
|
||||
List<String> inCallUuids = sameEraId ? Stream.of(peekJoinedUuids).map(UUID::toString).toList()
|
||||
: Collections.emptyList();
|
||||
|
||||
String body = GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids);
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(BODY, body);
|
||||
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(record.getId()));
|
||||
|
||||
return sameEraId;
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull Pair<Long, Long> insertCallLog(@NonNull RecipientId recipientId, long type, boolean unread, long timestamp) {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
|
||||
@@ -164,6 +164,10 @@ public abstract class DisplayRecord {
|
||||
return SmsDatabase.Types.isMissedVideoCall(type);
|
||||
}
|
||||
|
||||
public final boolean isGroupCall() {
|
||||
return SmsDatabase.Types.isGroupCall(type);
|
||||
}
|
||||
|
||||
public boolean isVerificationStatusChange() {
|
||||
return SmsDatabase.Types.isIdentityDefault(type) || SmsDatabase.Types.isIdentityVerified(type);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public final class GroupCallUpdateDetailsUtil {
|
||||
|
||||
private static final String TAG = Log.tag(GroupCallUpdateDetailsUtil.class);
|
||||
|
||||
private GroupCallUpdateDetailsUtil() {
|
||||
}
|
||||
|
||||
public static @NonNull GroupCallUpdateDetails parse(@Nullable String body) {
|
||||
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetails.getDefaultInstance();
|
||||
|
||||
if (body == null) {
|
||||
return groupCallUpdateDetails;
|
||||
}
|
||||
|
||||
try {
|
||||
groupCallUpdateDetails = GroupCallUpdateDetails.parseFrom(Base64.decode(body));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Group call update details could not be read", e);
|
||||
}
|
||||
|
||||
return groupCallUpdateDetails;
|
||||
}
|
||||
|
||||
public static @NonNull String createUpdatedBody(@NonNull GroupCallUpdateDetails groupCallUpdateDetails, @NonNull List<String> inCallUuids) {
|
||||
GroupCallUpdateDetails.Builder builder = groupCallUpdateDetails.toBuilder()
|
||||
.clearInCallUuids();
|
||||
|
||||
if (Util.hasItems(inCallUuids)) {
|
||||
builder.addAllInCallUuids(inCallUuids);
|
||||
}
|
||||
|
||||
return Base64.encodeBytes(builder.build().toByteArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Create a group call update message based on time and joined members.
|
||||
*/
|
||||
public class GroupCallUpdateMessageFactory implements UpdateDescription.StringFactory {
|
||||
private final Context context;
|
||||
private final List<UUID> joinedMembers;
|
||||
private final GroupCallUpdateDetails groupCallUpdateDetails;
|
||||
private final UUID selfUuid;
|
||||
|
||||
public GroupCallUpdateMessageFactory(@NonNull Context context, @NonNull List<UUID> joinedMembers, @NonNull GroupCallUpdateDetails groupCallUpdateDetails) {
|
||||
this.context = context;
|
||||
this.joinedMembers = new ArrayList<>(joinedMembers);
|
||||
this.groupCallUpdateDetails = groupCallUpdateDetails;
|
||||
this.selfUuid = TextSecurePreferences.getLocalUuid(context);
|
||||
|
||||
boolean removed = this.joinedMembers.remove(selfUuid);
|
||||
if (removed) {
|
||||
this.joinedMembers.add(selfUuid);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String create() {
|
||||
String time = DateUtils.getTimeString(context, Locale.getDefault(), groupCallUpdateDetails.getStartedCallTimestamp());
|
||||
|
||||
switch (joinedMembers.size()) {
|
||||
case 0:
|
||||
return context.getString(R.string.MessageRecord_group_call_s, time);
|
||||
case 1:
|
||||
if (joinedMembers.get(0).toString().equals(groupCallUpdateDetails.getStartedCallUuid())) {
|
||||
return context.getString(R.string.MessageRecord_s_started_a_group_call_s, describe(joinedMembers.get(0)), time);
|
||||
} else if (Objects.equals(joinedMembers.get(0), selfUuid)) {
|
||||
return context.getString(R.string.MessageRecord_you_are_in_the_group_call_s, describe(joinedMembers.get(0)), time);
|
||||
} else {
|
||||
return context.getString(R.string.MessageRecord_s_is_in_the_group_call_s, describe(joinedMembers.get(0)), time);
|
||||
}
|
||||
case 2:
|
||||
return context.getString(R.string.MessageRecord_s_and_s_are_in_the_group_call_s,
|
||||
describe(joinedMembers.get(0)),
|
||||
describe(joinedMembers.get(1)),
|
||||
time);
|
||||
default:
|
||||
int others = joinedMembers.size() - 2;
|
||||
return context.getResources().getQuantityString(R.plurals.MessageRecord_s_s_and_d_others_are_in_the_group_call_s,
|
||||
others,
|
||||
describe(joinedMembers.get(0)),
|
||||
describe(joinedMembers.get(1)),
|
||||
others,
|
||||
time);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull String describe(@NonNull UUID uuid) {
|
||||
if (UuidUtil.UNKNOWN_UUID.equals(uuid)) {
|
||||
return context.getString(R.string.MessageRecord_unknown);
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.resolved(RecipientId.from(uuid, null));
|
||||
|
||||
if (recipient.isSelf()) {
|
||||
return context.getString(R.string.MessageRecord_you);
|
||||
} else {
|
||||
return recipient.getShortDisplayName(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
@@ -35,6 +37,7 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
@@ -154,6 +157,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_audio_call_date, getCallDateString(context)), R.drawable.ic_update_audio_call_missed_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red));
|
||||
} else if (isMissedVideoCall()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_video_call_date, getCallDateString(context)), R.drawable.ic_update_video_call_missed_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red));
|
||||
} else if (isGroupCall()) {
|
||||
return getGroupCallUpdateDescription(context, getBody());
|
||||
} else if (isJoined()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().getDisplayName(context)), R.drawable.ic_update_group_add_16);
|
||||
} else if (isExpirationTimerUpdate()) {
|
||||
@@ -281,6 +286,19 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return context.getString(R.string.MessageRecord_changed_their_profile, getIndividualRecipient().getDisplayName(context));
|
||||
}
|
||||
|
||||
public static @NonNull UpdateDescription getGroupCallUpdateDescription(@NonNull Context context, @NonNull String body) {
|
||||
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(body);
|
||||
|
||||
List<UUID> joinedMembers = Stream.of(groupCallUpdateDetails.getInCallUuidsList())
|
||||
.map(UuidUtil::parseOrNull)
|
||||
.withoutNulls()
|
||||
.toList();
|
||||
|
||||
UpdateDescription.StringFactory stringFactory = new GroupCallUpdateMessageFactory(context, joinedMembers, groupCallUpdateDetails);
|
||||
|
||||
return UpdateDescription.mentioning(joinedMembers, stringFactory, R.drawable.ic_video_16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a UUID by it's corresponding recipient's {@link Recipient#getDisplayName(Context)}.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user