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