Add additional Group Calling features.

This commit is contained in:
Cody Henthorne
2020-11-20 15:42:46 -05:00
committed by GitHub
parent 8c1737e597
commit b90a74d26a
61 changed files with 1193 additions and 134 deletions

View File

@@ -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);

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}
}

View File

@@ -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)}.
*/