diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt index b634988ad7..4545408082 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt @@ -605,22 +605,6 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl return handleCallLinkUpdate(callRecipient, timestamp, CallId.fromEra(eraId), Direction.INCOMING) } - fun insertOrUpdateGroupCallFromExternalEvent( - groupRecipientId: RecipientId, - sender: RecipientId, - timestamp: Long, - messageGroupCallEraId: String? - ) { - insertOrUpdateGroupCallFromLocalEvent( - groupRecipientId, - sender, - timestamp, - messageGroupCallEraId, - emptyList(), - false - ) - } - fun insertOrUpdateGroupCallFromLocalEvent( groupRecipientId: RecipientId, sender: RecipientId, diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java index ffa70fbae0..fff9b2e450 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java @@ -44,7 +44,7 @@ public class JobManager implements ConstraintObserver.Notifier { private static final String TAG = Log.tag(JobManager.class); - public static final int CURRENT_VERSION = 11; + public static final int CURRENT_VERSION = 12; private final Application application; private final Configuration configuration; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JsonJobData.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JsonJobData.java index 159e0722f6..049d8f52b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JsonJobData.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JsonJobData.java @@ -50,6 +50,18 @@ public class JsonJobData { } } + public static @Nullable JsonJobData deserializeOrNull(@Nullable byte[] data) { + if (data == null) { + return null; + } + + try { + return JsonUtils.fromJson(data, JsonJobData.class); + } catch (IOException e) { + return null; + } + } + private JsonJobData(@JsonProperty("strings") @NonNull Map strings, @JsonProperty("stringArrays") @NonNull Map stringArrays, @JsonProperty("integers") @NonNull Map integers, diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/GroupCallPeekJobDataMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/GroupCallPeekJobDataMigration.kt new file mode 100644 index 0000000000..4186b3aa41 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/GroupCallPeekJobDataMigration.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.jobmanager.migrations + +import org.thoughtcrime.securesms.jobmanager.JobMigration +import org.thoughtcrime.securesms.jobmanager.JsonJobData +import org.thoughtcrime.securesms.jobs.protos.GroupCallPeekJobData +import org.thoughtcrime.securesms.recipients.RecipientId + +/** + * Migrate jobs with just the recipient id to utilize the new data proto. + */ +class GroupCallPeekJobDataMigration : JobMigration(12) { + + companion object { + private const val KEY_GROUP_RECIPIENT_ID: String = "group_recipient_id" + private val GROUP_PEEK_JOB_KEYS = arrayOf("GroupCallPeekJob", "GroupCallPeekWorkerJob") + } + + override fun migrate(jobData: JobData): JobData { + if (jobData.factoryKey !in GROUP_PEEK_JOB_KEYS) { + return jobData + } + + val data = jobData.data ?: return jobData + val jsonData = JsonJobData.deserializeOrNull(data) ?: return jobData + val recipientId = jsonData.getStringOrDefault(KEY_GROUP_RECIPIENT_ID, null) ?: return jobData + + val jobProto = GroupCallPeekJobData( + groupRecipientId = recipientId.toLong(), + senderRecipientId = RecipientId.UNKNOWN.toLong(), + serverTimestamp = 0L + ) + + return jobData.withData(jobProto.encode()) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallPeekJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallPeekJob.java index 6212301ff1..103576836b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallPeekJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallPeekJob.java @@ -4,11 +4,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint; -import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.jobs.protos.GroupCallPeekJobData; + +import java.io.IOException; /** * Allows the enqueueing of one peek operation per group while the web socket is not drained. @@ -19,32 +21,30 @@ public final class GroupCallPeekJob extends BaseJob { private static final String QUEUE = "__GroupCallPeekJob__"; - private static final String KEY_GROUP_RECIPIENT_ID = "group_recipient_id"; + @NonNull private final GroupCallPeekJobData groupCallPeekJobData; - @NonNull private final RecipientId groupRecipientId; - - public static void enqueue(@NonNull RecipientId groupRecipientId) { + public static void enqueue(@NonNull GroupCallPeekJobData groupCallPeekJobData) { JobManager jobManager = AppDependencies.getJobManager(); - String queue = QUEUE + groupRecipientId.serialize(); + String queue = QUEUE + groupCallPeekJobData.groupRecipientId; Parameters.Builder parameters = new Parameters.Builder() .setQueue(queue) .addConstraint(DecryptionsDrainedConstraint.KEY); jobManager.cancelAllInQueue(queue); - jobManager.add(new GroupCallPeekJob(parameters.build(), groupRecipientId)); + jobManager.add(new GroupCallPeekJob(parameters.build(), groupCallPeekJobData)); } private GroupCallPeekJob(@NonNull Parameters parameters, - @NonNull RecipientId groupRecipientId) + @NonNull GroupCallPeekJobData groupCallPeekJobData) { super(parameters); - this.groupRecipientId = groupRecipientId; + this.groupCallPeekJobData = groupCallPeekJobData; } @Override protected void onRun() { - AppDependencies.getJobManager().add(new GroupCallPeekWorkerJob(groupRecipientId)); + AppDependencies.getJobManager().add(new GroupCallPeekWorkerJob(groupCallPeekJobData)); } @Override @@ -53,10 +53,8 @@ public final class GroupCallPeekJob extends BaseJob { } @Override - public @Nullable byte[] serialize() { - return new JsonJobData.Builder() - .putString(KEY_GROUP_RECIPIENT_ID, groupRecipientId.serialize()) - .serialize(); + public @NonNull byte[] serialize() { + return groupCallPeekJobData.encode(); } @Override @@ -72,8 +70,12 @@ public final class GroupCallPeekJob extends BaseJob { @Override public @NonNull GroupCallPeekJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) { - JsonJobData data = JsonJobData.deserialize(serializedData); - return new GroupCallPeekJob(parameters, RecipientId.from(data.getString(KEY_GROUP_RECIPIENT_ID))); + try { + GroupCallPeekJobData jobData = GroupCallPeekJobData.ADAPTER.decode(serializedData); + return new GroupCallPeekJob(parameters, jobData); + } catch (IOException e) { + throw new RuntimeException(e); + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallPeekWorkerJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallPeekWorkerJob.java index a3bf355912..e648a4581f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallPeekWorkerJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallPeekWorkerJob.java @@ -3,10 +3,15 @@ package org.thoughtcrime.securesms.jobs; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; +import org.thoughtcrime.securesms.jobs.protos.GroupCallPeekJobData; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.service.webrtc.WebRtcUtil; + +import java.io.IOException; /** * Runs in the same queue as messages for the group. @@ -15,26 +20,42 @@ final class GroupCallPeekWorkerJob extends BaseJob { public static final String KEY = "GroupCallPeekWorkerJob"; - private static final String KEY_GROUP_RECIPIENT_ID = "group_recipient_id"; + private static final String KEY_GROUP_CALL_JOB_DATA = "group_call_job_data"; - @NonNull private final RecipientId groupRecipientId; + @NonNull private final GroupCallPeekJobData groupCallPeekJobData; - public GroupCallPeekWorkerJob(@NonNull RecipientId groupRecipientId) { + public GroupCallPeekWorkerJob(@NonNull GroupCallPeekJobData groupCallPeekJobData) { this(new Parameters.Builder() - .setQueue(PushProcessMessageJob.getQueueName(groupRecipientId)) + .setQueue(PushProcessMessageJob.getQueueName(RecipientId.from(groupCallPeekJobData.groupRecipientId))) .setMaxInstancesForQueue(2) .build(), - groupRecipientId); + groupCallPeekJobData); } - private GroupCallPeekWorkerJob(@NonNull Parameters parameters, @NonNull RecipientId groupRecipientId) { + private GroupCallPeekWorkerJob(@NonNull Parameters parameters, @NonNull GroupCallPeekJobData groupCallPeekJobData) { super(parameters); - this.groupRecipientId = groupRecipientId; + this.groupCallPeekJobData = groupCallPeekJobData; } @Override protected void onRun() { - AppDependencies.getSignalCallManager().peekGroupCall(groupRecipientId); + RecipientId groupRecipientId = RecipientId.from(groupCallPeekJobData.groupRecipientId); + + AppDependencies.getSignalCallManager().peekGroupCall(groupRecipientId, (peekInfo) -> { + if (groupCallPeekJobData.senderRecipientId == RecipientId.UNKNOWN.toLong()) { + return; + } + + RecipientId senderRecipientId = RecipientId.from(groupCallPeekJobData.senderRecipientId); + SignalDatabase.calls().insertOrUpdateGroupCallFromLocalEvent( + groupRecipientId, + senderRecipientId, + groupCallPeekJobData.serverTimestamp, + peekInfo.getEraId(), + peekInfo.getJoinedMembers(), + WebRtcUtil.isCallFull(peekInfo) + ); + }); } @Override @@ -43,10 +64,8 @@ final class GroupCallPeekWorkerJob extends BaseJob { } @Override - public @Nullable byte[] serialize() { - return new JsonJobData.Builder() - .putString(KEY_GROUP_RECIPIENT_ID, groupRecipientId.serialize()) - .serialize(); + public @NonNull byte[] serialize() { + return groupCallPeekJobData.encode(); } @Override @@ -62,8 +81,12 @@ final class GroupCallPeekWorkerJob extends BaseJob { @Override public @NonNull GroupCallPeekWorkerJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) { - JsonJobData data = JsonJobData.deserialize(serializedData); - return new GroupCallPeekWorkerJob(parameters, RecipientId.from(data.getString(KEY_GROUP_RECIPIENT_ID))); + try { + GroupCallPeekJobData jobData = GroupCallPeekJobData.ADAPTER.decode(serializedData); + return new GroupCallPeekWorkerJob(parameters, jobData); + } catch (IOException e) { + throw new RuntimeException(e); + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 60342339a4..77ed7eda7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.WifiConstraint; import org.thoughtcrime.securesms.jobmanager.migrations.DonationReceiptRedemptionJobMigration; +import org.thoughtcrime.securesms.jobmanager.migrations.GroupCallPeekJobDataMigration; import org.thoughtcrime.securesms.jobmanager.migrations.PushDecryptMessageJobEnvelopeMigration; import org.thoughtcrime.securesms.jobmanager.migrations.PushProcessMessageJobMigration; import org.thoughtcrime.securesms.jobmanager.migrations.PushProcessMessageQueueJobMigration; @@ -380,6 +381,7 @@ public final class JobManagerFactories { new PushDecryptMessageJobEnvelopeMigration(), new SenderKeyDistributionSendJobRecipientMigration(), new PushProcessMessageJobMigration(), - new DonationReceiptRedemptionJobMigration()); + new DonationReceiptRedemptionJobMigration(), + new GroupCallPeekJobDataMigration()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt index acd6d7ffea..ee092022de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt @@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.jobs.RefreshAttributesJob import org.thoughtcrime.securesms.jobs.RetrieveProfileJob import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob import org.thoughtcrime.securesms.jobs.TrimThreadJob +import org.thoughtcrime.securesms.jobs.protos.GroupCallPeekJobData import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.linkpreview.LinkPreview import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil @@ -1016,14 +1017,13 @@ object DataMessageProcessor { val groupRecipientId = SignalDatabase.recipients.getOrInsertFromPossiblyMigratedGroupId(groupId) - SignalDatabase.calls.insertOrUpdateGroupCallFromExternalEvent( - groupRecipientId, - senderRecipientId, - envelope.serverTimestamp!!, - groupCallUpdate.eraId + GroupCallPeekJob.enqueue( + GroupCallPeekJobData( + groupRecipientId.toLong(), + senderRecipientId.toLong(), + envelope.serverTimestamp!! + ) ) - - GroupCallPeekJob.enqueue(groupRecipientId) } fun notifyTypingStoppedFromIncomingMessage(context: Context, senderRecipient: Recipient, threadRecipientId: RecipientId, device: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java index 80ad3166cc..e44a40864b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java @@ -103,6 +103,7 @@ import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Consumer; import java.util.stream.Collectors; import io.reactivex.rxjava3.core.Flowable; @@ -446,6 +447,10 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. } public void peekGroupCall(@NonNull RecipientId id) { + peekGroupCall(id, null); + } + + public void peekGroupCall(@NonNull RecipientId id, @Nullable Consumer onWillUpdateCallFromPeek) { if (callManager == null) { Log.i(TAG, "Unable to peekGroupCall, call manager is null"); return; @@ -464,6 +469,10 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. Long threadId = SignalDatabase.threads().getThreadIdFor(group.getId()); if (threadId != null) { + if (onWillUpdateCallFromPeek != null) { + onWillUpdateCallFromPeek.accept(peekInfo); + } + SignalDatabase.calls() .updateGroupCallFromPeek(threadId, peekInfo.getEraId(), diff --git a/app/src/main/protowire/JobData.proto b/app/src/main/protowire/JobData.proto index 6fc7744e87..ca35357588 100644 --- a/app/src/main/protowire/JobData.proto +++ b/app/src/main/protowire/JobData.proto @@ -111,4 +111,10 @@ message DeleteSyncJobData { message Svr3MirrorJobData { optional string serializedChangeSession = 1; +} + +message GroupCallPeekJobData { + uint64 groupRecipientId = 1; + uint64 senderRecipientId = 2; + uint64 serverTimestamp = 3; } \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/GroupCallPeekJobDataMigrationTest.kt b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/GroupCallPeekJobDataMigrationTest.kt new file mode 100644 index 0000000000..0f5bf75bb3 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/GroupCallPeekJobDataMigrationTest.kt @@ -0,0 +1,89 @@ +package org.thoughtcrime.securesms.jobmanager.migrations + +import org.junit.Test +import org.thoughtcrime.securesms.assertIs +import org.thoughtcrime.securesms.jobmanager.JobMigration +import org.thoughtcrime.securesms.jobmanager.JsonJobData +import org.thoughtcrime.securesms.jobs.protos.GroupCallPeekJobData +import org.thoughtcrime.securesms.recipients.Recipient + +class GroupCallPeekJobDataMigrationTest { + + private val testSubject = GroupCallPeekJobDataMigration() + + @Test + fun `given an un-migrated GroupCallPeekJob, when I migrate, then I expect updated data`() { + val groupRecipientId = 1L + val jobData = createJobData(groupRecipientId = groupRecipientId) + val result = testSubject.migrate(jobData) + + val data = GroupCallPeekJobData.ADAPTER.decode(result.data!!) + + data.groupRecipientId assertIs groupRecipientId + data.senderRecipientId assertIs Recipient.UNKNOWN.id.toLong() + data.serverTimestamp assertIs 0L + } + + @Test + fun `given an un-migrated GroupCallPeekWorkerJob, when I migrate, then I expect updated data`() { + val groupRecipientId = 1L + val jobData = createJobData(factoryKey = "GroupCallPeekWorkerJob", groupRecipientId = groupRecipientId) + val result = testSubject.migrate(jobData) + + val data = GroupCallPeekJobData.ADAPTER.decode(result.data!!) + + data.groupRecipientId assertIs groupRecipientId + data.senderRecipientId assertIs Recipient.UNKNOWN.id.toLong() + data.serverTimestamp assertIs 0L + } + + @Test + fun `given an un-migrated ASDF, when I migrate, then I expect unchanged job data`() { + val groupRecipientId = 1L + val jobData = createJobData(factoryKey = "ASDF", groupRecipientId = groupRecipientId) + val result = testSubject.migrate(jobData) + + result assertIs jobData + } + + @Test + fun `given an un-migrated job without data, when I migrate, then I expect unchanged job data`() { + val groupRecipientId = 1L + val jobData = createJobData(groupRecipientId = groupRecipientId, data = null) + val result = testSubject.migrate(jobData) + + result assertIs jobData + } + + @Test + fun `given an un-migrated job with incorrect data, when I migrate, then I expect unchanged job data`() { + val groupRecipientId = 1L + val jobData = createJobData(groupRecipientId = groupRecipientId, data = JsonJobData.Builder().putString("asdf", groupRecipientId.toString()).serialize()) + val result = testSubject.migrate(jobData) + + result assertIs jobData + } + + @Test + fun `given an un-migrated job with un-deserializable data, when I migrate, then I expect unchanged job data`() { + val groupRecipientId = 1L + val jobData = createJobData(groupRecipientId = groupRecipientId, data = GroupCallPeekJobData().encode()) + val result = testSubject.migrate(jobData) + + result assertIs jobData + } + + private fun createJobData( + factoryKey: String = "GroupCallPeekJob", + groupRecipientId: Long, + data: ByteArray? = JsonJobData.Builder().putString("group_recipient_id", groupRecipientId.toString()).serialize() + ): JobMigration.JobData { + return JobMigration.JobData( + factoryKey = factoryKey, + queueKey = null, + maxAttempts = 0, + lifespan = 0, + data = data + ) + } +}