Send 'clear history' event when clearing the call log.

This commit is contained in:
Alex Hart 2023-08-01 16:13:37 -03:00 committed by Greyson Parrelli
parent d3f073e573
commit e239036d8b
11 changed files with 319 additions and 42 deletions

View file

@ -10,6 +10,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.ringrtc.CallLinkState
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.CallLinkUpdateSendJob
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
@ -58,6 +59,7 @@ class UpdateCallLinkRepository(
return { result ->
if (result is UpdateCallLinkResult.Success) {
SignalDatabase.callLinks.updateCallLinkState(credentials.roomId, result.state)
ApplicationDependencies.getJobManager().add(CallLinkUpdateSendJob(credentials.roomId))
}
}
}

View file

@ -5,11 +5,14 @@ import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.CallLinkPeekJob
import org.thoughtcrime.securesms.jobs.CallLogEventSendJob
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
@ -80,6 +83,23 @@ class CallLogRepository(
}.subscribeOn(Schedulers.io())
}
/**
* Delete all call events / unowned links and enqueue clear history job, and then
* emit a clear history message.
*/
fun deleteAllCallLogsOnOrBeforeNow(): Single<Int> {
return Single.fromCallable {
SignalDatabase.rawDatabase.withinTransaction {
val now = System.currentTimeMillis()
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(now)
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(now)
ApplicationDependencies.getJobManager().add(CallLogEventSendJob.forClearHistory(now))
}
SignalDatabase.callLinks.getAllAdminCallLinksExcept(emptySet())
}.flatMap(this::revokeAndCollectResults).map { -1 }.subscribeOn(Schedulers.io())
}
/**
* Deletes the selected call links. We DELETE those links we don't have admin keys for,
* and revoke the ones we *do* have admin keys for. We then perform a cleanup step on
@ -93,19 +113,7 @@ class CallLogRepository(
val allCallLinkIds = SignalDatabase.calls.getCallLinkRoomIdsFromCallRowIds(selectedCallRowIds) + selectedRoomIds
SignalDatabase.callLinks.deleteNonAdminCallLinks(allCallLinkIds)
SignalDatabase.callLinks.getAdminCallLinks(allCallLinkIds)
}.flatMap { callLinksToRevoke ->
Single.merge(
callLinksToRevoke.map {
updateCallLinkRepository.revokeCallLink(it.credentials!!)
}
).reduce(0) { acc, current ->
acc + (if (current is UpdateCallLinkResult.Success) 0 else 1)
}
}.doOnTerminate {
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}.doOnDispose {
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}.subscribeOn(Schedulers.io())
}.flatMap(this::revokeAndCollectResults).subscribeOn(Schedulers.io())
}
/**
@ -121,19 +129,21 @@ class CallLogRepository(
val allCallLinkIds = SignalDatabase.calls.getCallLinkRoomIdsFromCallRowIds(selectedCallRowIds) + selectedRoomIds
SignalDatabase.callLinks.deleteAllNonAdminCallLinksExcept(allCallLinkIds)
SignalDatabase.callLinks.getAllAdminCallLinksExcept(allCallLinkIds)
}.flatMap { callLinksToRevoke ->
Single.merge(
callLinksToRevoke.map {
updateCallLinkRepository.revokeCallLink(it.credentials!!)
}
).reduce(0) { acc, current ->
acc + (if (current is UpdateCallLinkResult.Success) 0 else 1)
}.flatMap(this::revokeAndCollectResults).subscribeOn(Schedulers.io())
}
private fun revokeAndCollectResults(callLinksToRevoke: Set<CallLinkTable.CallLink>): Single<Int> {
return Single.merge(
callLinksToRevoke.map {
updateCallLinkRepository.revokeCallLink(it.credentials!!)
}
).reduce(0) { acc, current ->
acc + (if (current is UpdateCallLinkResult.Success) 0 else 1)
}.doOnTerminate {
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}.doOnDispose {
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}.subscribeOn(Schedulers.io())
}
}
fun peekCallLinks(): Completable {

View file

@ -3,17 +3,17 @@ package org.thoughtcrime.securesms.calls.log
/**
* Selection state object for call logs.
*/
sealed class CallLogSelectionState {
abstract fun contains(callId: CallLogRow.Id): Boolean
abstract fun isNotEmpty(totalCount: Int): Boolean
sealed interface CallLogSelectionState {
fun contains(callId: CallLogRow.Id): Boolean
fun isNotEmpty(totalCount: Int): Boolean
abstract fun count(totalCount: Int): Int
fun count(totalCount: Int): Int
abstract fun selected(): Set<CallLogRow.Id>
fun selected(): Set<CallLogRow.Id>
fun isExclusionary(): Boolean = this is Excludes
protected abstract fun select(callId: CallLogRow.Id): CallLogSelectionState
protected abstract fun deselect(callId: CallLogRow.Id): CallLogSelectionState
fun select(callId: CallLogRow.Id): CallLogSelectionState
fun deselect(callId: CallLogRow.Id): CallLogSelectionState
fun toggle(callId: CallLogRow.Id): CallLogSelectionState {
return if (contains(callId)) {
@ -26,7 +26,7 @@ sealed class CallLogSelectionState {
/**
* Includes contains an opt-in list of call logs.
*/
data class Includes(private val includes: Set<CallLogRow.Id>) : CallLogSelectionState() {
data class Includes(private val includes: Set<CallLogRow.Id>) : CallLogSelectionState {
override fun contains(callId: CallLogRow.Id): Boolean {
return includes.contains(callId)
}
@ -55,7 +55,7 @@ sealed class CallLogSelectionState {
/**
* Excludes contains an opt-out list of call logs.
*/
data class Excludes(private val excluded: Set<CallLogRow.Id>) : CallLogSelectionState() {
data class Excludes(private val excluded: Set<CallLogRow.Id>) : CallLogSelectionState {
override fun contains(callId: CallLogRow.Id): Boolean = !excluded.contains(callId)
override fun isNotEmpty(totalCount: Int): Boolean = excluded.size < totalCount
@ -74,8 +74,10 @@ sealed class CallLogSelectionState {
override fun selected(): Set<CallLogRow.Id> = excluded
}
object All : CallLogSelectionState by Excludes(emptySet())
companion object {
fun empty(): CallLogSelectionState = Includes(emptySet())
fun selectAll(): CallLogSelectionState = Excludes(emptySet())
fun selectAll(): CallLogSelectionState = All
}
}

View file

@ -35,14 +35,21 @@ class CallLogStagedDeletion(
.map { it.roomId }
.toSet()
return if (stateSnapshot.isExclusionary()) {
repository.deleteAllCallLogsExcept(callRowIds, filter == CallLogFilter.MISSED).andThen(
repository.deleteAllCallLinksExcept(callRowIds, callLinkIds)
)
} else {
repository.deleteSelectedCallLogs(callRowIds).andThen(
repository.deleteSelectedCallLinks(callRowIds, callLinkIds)
)
return when {
stateSnapshot is CallLogSelectionState.All && filter == CallLogFilter.ALL -> {
repository.deleteAllCallLogsOnOrBeforeNow()
}
stateSnapshot is CallLogSelectionState.Excludes || stateSnapshot is CallLogSelectionState.All -> {
repository.deleteAllCallLogsExcept(callRowIds, filter == CallLogFilter.MISSED).andThen(
repository.deleteAllCallLinksExcept(callRowIds, callLinkIds)
)
}
stateSnapshot is CallLogSelectionState.Includes -> {
repository.deleteSelectedCallLogs(callRowIds).andThen(
repository.deleteSelectedCallLinks(callRowIds, callLinkIds)
)
}
else -> error("Unhandled state $stateSnapshot $filter")
}
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.jobs
import com.google.protobuf.ByteString
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.protos.CallLinkUpdateSendJobData
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.util.FeatureFlags
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallLinkUpdate
import java.lang.Exception
import java.util.Optional
import java.util.concurrent.TimeUnit
/**
* Sends a [CallLinkUpdate] message to linked devices.
*/
class CallLinkUpdateSendJob private constructor(
parameters: Parameters,
private val callLinkRoomId: CallLinkRoomId
) : BaseJob(parameters) {
companion object {
const val KEY = "CallLinkUpdateSendJob"
private val TAG = Log.tag(CallLinkUpdateSendJob::class.java)
}
constructor(
callLinkRoomId: CallLinkRoomId
) : this(
Parameters.Builder()
.setQueue("CallLinkUpdateSendJob")
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.addConstraint(NetworkConstraint.KEY)
.build(),
callLinkRoomId
)
override fun serialize(): ByteArray = CallLinkUpdateSendJobData.Builder()
.callLinkRoomId(callLinkRoomId.serialize())
.build()
.encode()
override fun getFactoryKey(): String = KEY
override fun onFailure() = Unit
override fun onRun() {
if (!FeatureFlags.adHocCalling()) {
Log.i(TAG, "Call links are not enabled. Exiting.")
return
}
val callLink = SignalDatabase.callLinks.getCallLinkByRoomId(callLinkRoomId)
if (callLink?.credentials == null) {
Log.i(TAG, "Call link not found or missing credentials. Exiting.")
return
}
val callLinkUpdate = CallLinkUpdate.newBuilder()
.setRootKey(ByteString.copyFrom(callLink.credentials.linkKeyBytes))
.build()
ApplicationDependencies.getSignalServiceMessageSender()
.sendSyncMessage(SignalServiceSyncMessage.forCallLinkUpdate(callLinkUpdate), Optional.empty())
}
override fun onShouldRetry(e: Exception): Boolean {
return when (e) {
is ServerRejectedException -> false
is PushNetworkException -> true
else -> false
}
}
class Factory : Job.Factory<CallLinkUpdateSendJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): CallLinkUpdateSendJob {
return CallLinkUpdateSendJob(
parameters,
CallLinkRoomId.DatabaseSerializer.deserialize(CallLinkUpdateSendJobData.ADAPTER.decode(serializedData!!).callLinkRoomId)
)
}
}
}

View file

@ -0,0 +1,83 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.jobs
import okio.ByteString
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.protos.CallLogEventSendJobData
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import java.util.Optional
import java.util.concurrent.TimeUnit
/**
* Sends CallLogEvents to synced devices.
*/
class CallLogEventSendJob private constructor(
parameters: Parameters,
private val callLogEvent: SignalServiceProtos.SyncMessage.CallLogEvent
) : BaseJob(parameters) {
companion object {
const val KEY = "CallLogEventSendJob"
fun forClearHistory(
timestamp: Long
) = CallLogEventSendJob(
Parameters.Builder()
.setQueue("CallLogEventSendJob")
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.addConstraint(NetworkConstraint.KEY)
.build(),
SignalServiceProtos.SyncMessage.CallLogEvent
.newBuilder()
.setTimestamp(timestamp)
.setType(SignalServiceProtos.SyncMessage.CallLogEvent.Type.CLEAR)
.build()
)
}
override fun serialize(): ByteArray = CallLogEventSendJobData.Builder()
.callLogEvent(ByteString.of(*callLogEvent.toByteArray()))
.build()
.encode()
override fun getFactoryKey(): String = KEY
override fun onFailure() = Unit
override fun onRun() {
ApplicationDependencies.getSignalServiceMessageSender()
.sendSyncMessage(
SignalServiceSyncMessage.forCallLogEvent(callLogEvent),
Optional.empty()
)
}
override fun onShouldRetry(e: Exception): Boolean {
return when (e) {
is ServerRejectedException -> false
is PushNetworkException -> true
else -> false
}
}
class Factory : Job.Factory<CallLogEventSendJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): CallLogEventSendJob {
return CallLogEventSendJob(
parameters,
SignalServiceProtos.SyncMessage.CallLogEvent.parseFrom(
CallLogEventSendJobData.ADAPTER.decode(serializedData!!).callLogEvent.toByteArray()
)
)
}
}
}

View file

@ -102,6 +102,8 @@ public final class JobManagerFactories {
put(AvatarGroupsV2DownloadJob.KEY, new AvatarGroupsV2DownloadJob.Factory());
put(BoostReceiptRequestResponseJob.KEY, new BoostReceiptRequestResponseJob.Factory());
put(CallLinkPeekJob.KEY, new CallLinkPeekJob.Factory());
put(CallLinkUpdateSendJob.KEY, new CallLinkUpdateSendJob.Factory());
put(CallLogEventSendJob.KEY, new CallLogEventSendJob.Factory());
put(CallSyncEventJob.KEY, new CallSyncEventJob.Factory());
put(CheckServiceReachabilityJob.KEY, new CheckServiceReachabilityJob.Factory());
put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory());

View file

@ -17,6 +17,7 @@ class CallLinkRoomId private constructor(private val roomId: ByteArray) : Parcel
fun serialize(): String = DatabaseSerializer.serialize(this)
fun encodeForProto(): ByteString = ByteString.copyFrom(roomId)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View file

@ -19,4 +19,12 @@ message CallSyncEventJobData {
message CallLinkRefreshSinceTimestampJobData {
uint64 timestamp = 1;
}
message CallLogEventSendJobData {
bytes callLogEvent = 1;
}
message CallLinkUpdateSendJobData {
string callLinkRoomId = 1;
}

View file

@ -743,6 +743,10 @@ public class SignalServiceMessageSender {
urgent = message.getRequest().get().isUrgent();
} else if (message.getCallEvent().isPresent()) {
content = createCallEventContent(message.getCallEvent().get());
} else if (message.getCallLinkUpdate().isPresent()) {
content = createCallLinkUpdateContent(message.getCallLinkUpdate().get());
} else if (message.getCallLogEvent().isPresent()) {
content = createCallLogEventContent(message.getCallLogEvent().get());
} else {
throw new IOException("Unsupported sync message!");
}
@ -1701,6 +1705,20 @@ public class SignalServiceMessageSender {
return container.setSyncMessage(builder).build();
}
private Content createCallLinkUpdateContent(SyncMessage.CallLinkUpdate proto) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder builder = createSyncMessageBuilder().setCallLinkUpdate(proto);
return container.setSyncMessage(builder).build();
}
private Content createCallLogEventContent(SyncMessage.CallLogEvent proto) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder builder = createSyncMessageBuilder().setCallLogEvent(proto);
return container.setSyncMessage(builder).build();
}
private SyncMessage.Builder createSyncMessageBuilder() {
SecureRandom random = new SecureRandom();
byte[] padding = Util.getRandomLengthBytes(512);

View file

@ -7,10 +7,10 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallEvent;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallLinkUpdate;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallLogEvent;
import java.util.LinkedList;
import java.util.List;
@ -36,6 +36,7 @@ public class SignalServiceSyncMessage {
private final Optional<List<ViewedMessage>> views;
private final Optional<CallEvent> callEvent;
private final Optional<CallLinkUpdate> callLinkUpdate;
private final Optional<CallLogEvent> callLogEvent;
private SignalServiceSyncMessage(Optional<SentTranscriptMessage> sent,
Optional<ContactsMessage> contacts,
@ -52,7 +53,8 @@ public class SignalServiceSyncMessage {
Optional<OutgoingPaymentMessage> outgoingPaymentMessage,
Optional<List<ViewedMessage>> views,
Optional<CallEvent> callEvent,
Optional<CallLinkUpdate> callLinkUpdate)
Optional<CallLinkUpdate> callLinkUpdate,
Optional<CallLogEvent> callLogEvent)
{
this.sent = sent;
this.contacts = contacts;
@ -70,6 +72,7 @@ public class SignalServiceSyncMessage {
this.views = views;
this.callEvent = callEvent;
this.callLinkUpdate = callLinkUpdate;
this.callLogEvent = callLogEvent;
}
public static SignalServiceSyncMessage forSentTranscript(SentTranscriptMessage sent) {
@ -88,6 +91,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -107,6 +111,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -126,6 +131,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -145,6 +151,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -164,6 +171,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.of(views),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -183,6 +191,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -205,6 +214,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -224,6 +234,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -243,6 +254,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -262,6 +274,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -281,6 +294,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -300,6 +314,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -319,6 +334,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -338,6 +354,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -357,6 +374,7 @@ public class SignalServiceSyncMessage {
Optional.of(outgoingPaymentMessage),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -376,6 +394,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.of(callEvent),
Optional.empty(),
Optional.empty());
}
@ -395,7 +414,28 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.of(callLinkUpdate));
Optional.of(callLinkUpdate),
Optional.empty());
}
public static SignalServiceSyncMessage forCallLogEvent(@Nonnull CallLogEvent callLogEvent) {
return new SignalServiceSyncMessage(Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.of(callLogEvent));
}
public static SignalServiceSyncMessage empty() {
@ -414,6 +454,7 @@ public class SignalServiceSyncMessage {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty());
}
@ -477,6 +518,14 @@ public class SignalServiceSyncMessage {
return callEvent;
}
public Optional<CallLinkUpdate> getCallLinkUpdate() {
return callLinkUpdate;
}
public Optional<CallLogEvent> getCallLogEvent() {
return callLogEvent;
}
public enum FetchType {
LOCAL_PROFILE,
STORAGE_MANIFEST,