Ensure owned call links are revoked on delete.

This commit is contained in:
Alex Hart 2023-06-13 11:06:05 -03:00 committed by Cody Henthorne
parent 03a212eee4
commit 290b0fe46f
12 changed files with 334 additions and 126 deletions

View file

@ -75,12 +75,10 @@ class CallLogAdapter(
fun submitCallRows(
rows: List<CallLogRow?>,
selectionState: CallLogSelectionState,
stagedDeletion: CallLogStagedDeletion?,
onCommit: () -> Unit
): Int {
val filteredRows = rows
.filterNotNull()
.filterNot { stagedDeletion?.isStagedForDeletion(it.id) == true }
.map {
when (it) {
is CallLogRow.Call -> CallModel(it, selectionState, itemCount)

View file

@ -0,0 +1,14 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.calls.log
sealed interface CallLogDeletionResult {
object Success : CallLogDeletionResult
object Empty : CallLogDeletionResult
data class FailedToRevoke(val failedRevocations: Int) : CallLogDeletionResult
data class UnknownFailure(val reason: Throwable) : CallLogDeletionResult
}

View file

@ -7,7 +7,9 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.coordinatorlayout.widget.CoordinatorLayout
@ -27,10 +29,13 @@ import io.reactivex.rxjava3.kotlin.Flowables
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.DimensionUnit
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.concurrent.addTo
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
import org.thoughtcrime.securesms.calls.new.NewCallActivity
import org.thoughtcrime.securesms.components.Material3SearchToolbar
import org.thoughtcrime.securesms.components.ProgressCardDialogFragment
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.menu.ActionItem
@ -65,6 +70,10 @@ import java.util.concurrent.TimeUnit
@SuppressLint("DiscouragedApi")
class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Callbacks, CallLogContextMenu.Callbacks {
companion object {
private val TAG = Log.tag(CallLogFragment::class.java)
}
private val viewModel: CallLogViewModel by viewModels()
private val binding: CallLogFragmentBinding by ViewBinderDelegate(CallLogFragmentBinding::bind)
private val disposables = LifecycleDisposable()
@ -114,24 +123,23 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
)
disposables += scrollToPositionDelegate
disposables += Flowables.combineLatest(viewModel.data, viewModel.selectedAndStagedDeletion)
disposables += Flowables.combineLatest(viewModel.data, viewModel.selected)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { (data, selected) ->
val filteredCount = adapter.submitCallRows(
data,
selected.first,
selected.second,
selected,
scrollToPositionDelegate::notifyListCommitted
)
binding.emptyState.visible = filteredCount == 0
}
disposables += Flowables.combineLatest(viewModel.selectedAndStagedDeletion, viewModel.totalCount)
disposables += Flowables.combineLatest(viewModel.selected, viewModel.totalCount)
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { (selected, totalCount) ->
if (selected.first.isNotEmpty(totalCount)) {
callLogActionMode.setCount(selected.first.count(totalCount))
if (selected.isNotEmpty(totalCount)) {
callLogActionMode.setCount(selected.count(totalCount))
} else {
callLogActionMode.end()
}
@ -223,19 +231,8 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
MaterialAlertDialogBuilder(requireContext())
.setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, count, count))
.setPositiveButton(R.string.CallLogFragment__delete_for_me) { _, _ ->
viewModel.stageSelectionDeletion()
performDeletion(count, viewModel.stageSelectionDeletion())
callLogActionMode.end()
Snackbar
.make(
binding.root,
resources.getQuantityString(R.plurals.CallLogFragment__d_calls_deleted, count, count),
Snackbar.LENGTH_SHORT
)
.addCallback(SnackbarDeletionCallback())
.setAction(R.string.CallLogFragment__undo) {
viewModel.cancelStagedDeletion()
}
.show()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
@ -272,6 +269,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
scrollToPositionDelegate.resetScrollPosition()
}
}
FilterPullState.OPENING -> {
ViewUtil.setMinimumHeight(collapsingToolbarLayout, openHeight)
viewModel.setFilter(CallLogFilter.MISSED)
@ -366,20 +364,8 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
MaterialAlertDialogBuilder(requireContext())
.setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, 1, 1))
.setPositiveButton(R.string.CallLogFragment__delete_for_me) { _, _ ->
viewModel.stageCallDeletion(call)
Snackbar
.make(
binding.root,
resources.getQuantityString(R.plurals.CallLogFragment__d_calls_deleted, 1, 1),
Snackbar.LENGTH_SHORT
)
.addCallback(SnackbarDeletionCallback())
.setAction(R.string.CallLogFragment__undo) {
viewModel.cancelStagedDeletion()
}
.show()
performDeletion(1, viewModel.stageCallDeletion(call))
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
@ -394,18 +380,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
.setMessage(R.string.CallLogFragment__this_will_permanently_delete_all_call_history)
.setPositiveButton(android.R.string.ok) { _, _ ->
callLogActionMode.end()
viewModel.stageDeleteAll()
Snackbar
.make(
binding.root,
R.string.CallLogFragment__cleared_call_history,
Snackbar.LENGTH_SHORT
)
.addCallback(SnackbarDeletionCallback())
.setAction(R.string.CallLogFragment__undo) {
viewModel.cancelStagedDeletion()
}
.show()
performDeletion(-1, viewModel.stageDeleteAll())
}
.setNegativeButton(android.R.string.cancel, null)
.show()
@ -426,7 +401,59 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
private fun isSearchVisible(): Boolean {
return requireListener<SearchBinder>().getSearchToolbar().resolved() &&
requireListener<SearchBinder>().getSearchToolbar().get().getVisibility() == View.VISIBLE
requireListener<SearchBinder>().getSearchToolbar().get().visibility == View.VISIBLE
}
private fun performDeletion(count: Int, callLogStagedDeletion: CallLogStagedDeletion) {
var progressDialog: ProgressCardDialogFragment? = null
var errorDialog: AlertDialog? = null
fun cleanUp() {
progressDialog?.dismissAllowingStateLoss()
progressDialog = null
errorDialog?.dismiss()
errorDialog = null
}
val snackbarMessage = if (count == -1) {
getString(R.string.CallLogFragment__cleared_call_history)
} else {
resources.getQuantityString(R.plurals.CallLogFragment__d_calls_deleted, count, count)
}
viewModel.delete(callLogStagedDeletion)
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
progressDialog = ProgressCardDialogFragment.create(getString(R.string.CallLogFragment__deleting))
progressDialog?.show(parentFragmentManager, null)
}
.doOnDispose { cleanUp() }
.subscribeBy {
cleanUp()
when (it) {
CallLogDeletionResult.Empty -> Unit
is CallLogDeletionResult.FailedToRevoke -> {
errorDialog = MaterialAlertDialogBuilder(requireContext())
.setMessage(resources.getQuantityString(R.plurals.CallLogFragment__cant_delete_call_link, it.failedRevocations))
.setPositiveButton(R.string.ok, null)
.show()
}
CallLogDeletionResult.Success -> {
Snackbar
.make(
binding.root,
snackbarMessage,
Snackbar.LENGTH_SHORT
)
.show()
}
is CallLogDeletionResult.UnknownFailure -> {
Log.w(TAG, "Deletion failed.", it.reason)
Toast.makeText(requireContext(), R.string.CallLogFragment__deletion_failed, Toast.LENGTH_SHORT).show()
}
}
}
.addTo(disposables)
}
private inner class BottomActionBarControllerCallback : SignalBottomActionBarController.Callback {
@ -454,12 +481,6 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
}
}
private inner class SnackbarDeletionCallback : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
viewModel.commitStagedDeletion()
}
}
interface Callback {
fun onMultiSelectStarted()
fun onMultiSelectFinished()

View file

@ -2,14 +2,20 @@ package org.thoughtcrime.securesms.calls.log
import io.reactivex.rxjava3.core.Completable
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.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository
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.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
class CallLogRepository : CallLogPagedDataSource.CallRepository {
class CallLogRepository(
private val updateCallLinkRepository: UpdateCallLinkRepository = UpdateCallLinkRepository()
) : CallLogPagedDataSource.CallRepository {
override fun getCallsCount(query: String?, filter: CallLogFilter): Int {
return SignalDatabase.calls.getCallsCount(query, filter)
}
@ -61,7 +67,7 @@ class CallLogRepository : CallLogPagedDataSource.CallRepository {
selectedCallRowIds: Set<Long>
): Completable {
return Completable.fromAction {
SignalDatabase.calls.deleteCallEvents(selectedCallRowIds)
SignalDatabase.calls.deleteNonAdHocCallEvents(selectedCallRowIds)
}.observeOn(Schedulers.io())
}
@ -70,7 +76,63 @@ class CallLogRepository : CallLogPagedDataSource.CallRepository {
missedOnly: Boolean
): Completable {
return Completable.fromAction {
SignalDatabase.calls.deleteAllCallEventsExcept(selectedCallRowIds, missedOnly)
SignalDatabase.calls.deleteAllNonAdHocCallEventsExcept(selectedCallRowIds, missedOnly)
}.observeOn(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
* terminate to clean up call events.
*/
fun deleteSelectedCallLinks(
selectedCallRowIds: Set<Long>,
selectedRoomIds: Set<CallLinkRoomId>
): Single<Int> {
return Single.fromCallable {
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()
}.observeOn(Schedulers.io())
}
/**
* Deletes all but 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
* terminate to clean up call events.
*/
fun deleteAllCallLinksExcept(
selectedCallRowIds: Set<Long>,
selectedRoomIds: Set<CallLinkRoomId>
): Single<Int> {
return Single.fromCallable {
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)
}
}.doOnTerminate {
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}.doOnDispose {
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}.observeOn(Schedulers.io())
}

View file

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.calls.log
import androidx.annotation.MainThread
import io.reactivex.rxjava3.core.Single
/**
* Encapsulates a single deletion action
@ -13,19 +14,13 @@ class CallLogStagedDeletion(
private var isCommitted = false
fun isStagedForDeletion(id: CallLogRow.Id): Boolean {
return stateSnapshot.contains(id)
}
/**
* Returns a Single<Int> which contains the number of failed call-link revocations.
*/
@MainThread
fun cancel() {
isCommitted = true
}
@MainThread
fun commit() {
fun commit(): Single<Int> {
if (isCommitted) {
return
return Single.just(0)
}
isCommitted = true
@ -35,10 +30,19 @@ class CallLogStagedDeletion(
.flatten()
.toSet()
if (stateSnapshot.isExclusionary()) {
repository.deleteAllCallLogsExcept(callRowIds, filter == CallLogFilter.MISSED).subscribe()
val callLinkIds = stateSnapshot.selected()
.filterIsInstance<CallLogRow.Id.CallLink>()
.map { it.roomId }
.toSet()
return if (stateSnapshot.isExclusionary()) {
repository.deleteAllCallLogsExcept(callRowIds, filter == CallLogFilter.MISSED).andThen(
repository.deleteAllCallLinksExcept(callRowIds, callLinkIds)
)
} else {
repository.deleteSelectedCallLogs(callRowIds).subscribe()
repository.deleteSelectedCallLogs(callRowIds).andThen(
repository.deleteSelectedCallLinks(callRowIds, callLinkIds)
)
}
}
}

View file

@ -1,9 +1,11 @@
package org.thoughtcrime.securesms.calls.log
import android.annotation.SuppressLint
import androidx.annotation.MainThread
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
@ -36,9 +38,7 @@ class CallLogViewModel(
val controller = ProxyPagingController<CallLogRow.Id>()
val data: Flowable<MutableList<CallLogRow?>> = pagedData.switchMap { it.data.toFlowable(BackpressureStrategy.LATEST) }
val selectedAndStagedDeletion: Flowable<Pair<CallLogSelectionState, CallLogStagedDeletion?>> = callLogStore
.stateFlowable
.map { it.selectionState to it.stagedDeletion }
val selected: Flowable<CallLogSelectionState> = callLogStore.stateFlowable.map { it.selectionState }
private val _isEmpty: BehaviorProcessor<Boolean> = BehaviorProcessor.createDefault(false)
val isEmpty: Boolean get() = _isEmpty.value ?: false
@ -98,7 +98,6 @@ class CallLogViewModel(
}
override fun onCleared() {
commitStagedDeletion()
disposables.dispose()
}
@ -121,63 +120,52 @@ class CallLogViewModel(
}
@MainThread
fun stageCallDeletion(call: CallLogRow) {
callLogStore.state.stagedDeletion?.commit()
callLogStore.update {
it.copy(
stagedDeletion = CallLogStagedDeletion(
it.filter,
CallLogSelectionState.empty().toggle(call.id),
callLogRepository
)
)
}
fun stageCallDeletion(call: CallLogRow): CallLogStagedDeletion {
return CallLogStagedDeletion(
callLogStore.state.filter,
CallLogSelectionState.empty().toggle(call.id),
callLogRepository
)
}
@MainThread
fun stageSelectionDeletion() {
callLogStore.state.stagedDeletion?.commit()
callLogStore.update {
it.copy(
stagedDeletion = CallLogStagedDeletion(
it.filter,
it.selectionState,
callLogRepository
)
)
}
fun stageSelectionDeletion(): CallLogStagedDeletion {
return CallLogStagedDeletion(
callLogStore.state.filter,
callLogStore.state.selectionState,
callLogRepository
)
}
fun stageDeleteAll() {
callLogStore.state.stagedDeletion?.cancel()
fun stageDeleteAll(): CallLogStagedDeletion {
callLogStore.update {
it.copy(
selectionState = CallLogSelectionState.empty(),
stagedDeletion = CallLogStagedDeletion(
it.filter,
CallLogSelectionState.selectAll(),
callLogRepository
)
selectionState = CallLogSelectionState.empty()
)
}
return CallLogStagedDeletion(
callLogStore.state.filter,
CallLogSelectionState.selectAll(),
callLogRepository
)
}
fun commitStagedDeletion() {
callLogStore.state.stagedDeletion?.commit()
callLogStore.update {
it.copy(
stagedDeletion = null
)
}
}
fun cancelStagedDeletion() {
callLogStore.state.stagedDeletion?.cancel()
callLogStore.update {
it.copy(
stagedDeletion = null
)
}
@SuppressLint("CheckResult")
fun delete(stagedDeletion: CallLogStagedDeletion): Maybe<CallLogDeletionResult> {
return stagedDeletion.commit()
.doOnSubscribe {
clearSelected()
}
.map { failedRevocations ->
if (failedRevocations == 0) {
CallLogDeletionResult.Success
} else {
CallLogDeletionResult.FailedToRevoke(failedRevocations)
}
}
.onErrorReturn { CallLogDeletionResult.UnknownFailure(it) }
.toMaybe()
}
fun clearSelected() {
@ -197,7 +185,6 @@ class CallLogViewModel(
private data class CallLogState(
val query: String? = null,
val filter: CallLogFilter = CallLogFilter.ALL,
val selectionState: CallLogSelectionState = CallLogSelectionState.empty(),
val stagedDeletion: CallLogStagedDeletion? = null
val selectionState: CallLogSelectionState = CallLogSelectionState.empty()
)
}

View file

@ -14,6 +14,14 @@ import org.thoughtcrime.securesms.R
*/
class ProgressCardDialogFragment : DialogFragment(R.layout.progress_card_dialog) {
companion object {
fun create(title: String): ProgressCardDialogFragment {
return ProgressCardDialogFragment().apply {
arguments = ProgressCardDialogFragmentArgs.Builder(title).build().toBundle()
}
}
}
private val args: ProgressCardDialogFragmentArgs by navArgs()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

View file

@ -6,6 +6,7 @@ import android.database.Cursor
import androidx.core.content.contentValuesOf
import org.signal.core.util.Serializer
import org.signal.core.util.SqlUtil
import org.signal.core.util.delete
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
import org.signal.core.util.readToList
@ -32,7 +33,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
import org.thoughtcrime.securesms.util.Base64
import java.time.Instant
import java.time.temporal.ChronoUnit
@ -221,6 +221,64 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
}
}
fun deleteNonAdminCallLinks(roomIds: Set<CallLinkRoomId>) {
val queries = SqlUtil.buildCollectionQuery(ROOM_ID, roomIds)
queries.forEach {
writableDatabase.delete(TABLE_NAME)
.where("${it.where} AND $ADMIN_KEY IS NULL", it.whereArgs)
.run()
}
}
fun getAdminCallLinks(roomIds: Set<CallLinkRoomId>): Set<CallLink> {
val queries = SqlUtil.buildCollectionQuery(ROOM_ID, roomIds)
return queries.map {
writableDatabase
.select()
.from(TABLE_NAME)
.where("${it.where} AND $ADMIN_KEY IS NOT NULL", it.whereArgs)
.run()
.readToList { CallLinkDeserializer.deserialize(it) }
}.flatten().toSet()
}
fun deleteAllNonAdminCallLinksExcept(roomIds: Set<CallLinkRoomId>) {
if (roomIds.isEmpty()) {
writableDatabase.delete(TABLE_NAME)
.where("$ADMIN_KEY IS NULL")
.run()
} else {
SqlUtil.buildCollectionQuery(ROOM_ID, roomIds, collectionOperator = SqlUtil.CollectionOperator.NOT_IN).forEach {
writableDatabase.delete(TABLE_NAME)
.where("${it.where} AND $ADMIN_KEY IS NULL", it.whereArgs)
.run()
}
}
}
fun getAllAdminCallLinksExcept(roomIds: Set<CallLinkRoomId>): Set<CallLink> {
return if (roomIds.isEmpty()) {
writableDatabase
.select()
.from(TABLE_NAME)
.where("$ADMIN_KEY IS NOT NULL")
.run()
.readToList { CallLinkDeserializer.deserialize(it) }
.toSet()
} else {
SqlUtil.buildCollectionQuery(ROOM_ID, roomIds, collectionOperator = SqlUtil.CollectionOperator.NOT_IN).map {
writableDatabase
.select()
.from(TABLE_NAME)
.where("${it.where} AND $ADMIN_KEY IS NOT NULL", it.whereArgs)
.run()
.readToList { CallLinkDeserializer.deserialize(it) }
}.flatten().toSet()
}
}
private fun queryCallLinks(query: String?, offset: Int, limit: Int, asCount: Boolean): Cursor {
//language=sql
val noCallEvent = """
@ -289,7 +347,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
override fun deserialize(data: Cursor): CallLink {
return CallLink(
recipientId = data.requireLong(RECIPIENT_ID).let { if (it > 0) RecipientId.from(it) else RecipientId.UNKNOWN },
roomId = CallLinkRoomId.fromBytes(Base64.decode(data.requireNonNullString(ROOM_ID))),
roomId = CallLinkRoomId.DatabaseSerializer.deserialize(data.requireNonNullString(ROOM_ID)),
credentials = CallLinkCredentials(
linkKeyBytes = data.requireNonNullBlob(ROOT_KEY),
adminPassBytes = data.requireBlob(ADMIN_KEY)

View file

@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.CallSyncEventJob
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallEvent
import java.util.UUID
@ -207,6 +208,48 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
.run()
}
fun getCallLinkRoomIdsFromCallRowIds(callRowIds: Set<Long>): Set<CallLinkRoomId> {
return SqlUtil.buildCollectionQuery("$TABLE_NAME.$ID", callRowIds).map { query ->
//language=sql
val statement = """
SELECT ${CallLinkTable.ROOM_ID} FROM $TABLE_NAME
INNER JOIN ${CallLinkTable.TABLE_NAME} ON ${CallLinkTable.TABLE_NAME}.${CallLinkTable.RECIPIENT_ID} = $PEER
WHERE $TYPE = ${Type.serialize(Type.AD_HOC_CALL)} AND ${query.where}
""".toSingleLine()
readableDatabase.query(statement, query.whereArgs).readToList {
CallLinkRoomId.DatabaseSerializer.deserialize(it.requireNonNullString(CallLinkTable.ROOM_ID))
}
}.flatten().toSet()
}
/**
* If a call link has been revoked, or if we do not have a CallLink table entry for an AD_HOC_CALL type
* event, we mark it deleted.
*/
fun updateAdHocCallEventDeletionTimestamps() {
//language=sql
val statement = """
UPDATE $TABLE_NAME
SET $DELETION_TIMESTAMP = ${System.currentTimeMillis()}, $EVENT = ${Event.serialize(Event.DELETE)}
WHERE $TYPE = ${Type.serialize(Type.AD_HOC_CALL)}
AND (
(NOT EXISTS (SELECT 1 FROM ${CallLinkTable.TABLE_NAME} WHERE ${CallLinkTable.RECIPIENT_ID} = $PEER))
OR
(SELECT ${CallLinkTable.REVOKED} FROM ${CallLinkTable.TABLE_NAME} WHERE ${CallLinkTable.RECIPIENT_ID} = $PEER)
)
RETURNING *
""".toSingleLine()
val toSync = writableDatabase.query(statement).readToList {
Call.deserialize(it)
}.toSet()
CallSyncEventJob.enqueueDeleteSyncEvents(toSync)
ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary()
ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
}
/**
* If a non-ad-hoc call has been deleted from the message database, then we need to
* set its deletion_timestamp to now.
@ -706,13 +749,13 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
.run()
}
fun deleteCallEvents(callRowIds: Set<Long>) {
fun deleteNonAdHocCallEvents(callRowIds: Set<Long>) {
val messageIds = getMessageIds(callRowIds)
SignalDatabase.messages.deleteCallUpdates(messageIds)
updateCallEventDeletionTimestamps()
}
fun deleteAllCallEventsExcept(callRowIds: Set<Long>, missedOnly: Boolean) {
fun deleteAllNonAdHocCallEventsExcept(callRowIds: Set<Long>, missedOnly: Boolean) {
val callFilter = if (missedOnly) {
"$EVENT = ${Event.serialize(Event.MISSED)} AND $DELETION_TIMESTAMP = 0"
} else {

View file

@ -32,6 +32,10 @@ class CallLinkRoomId private constructor(private val roomId: ByteArray) : Parcel
return roomId.contentHashCode()
}
override fun toString(): String {
return DatabaseSerializer.serialize(this)
}
object DatabaseSerializer : Serializer<CallLinkRoomId, String> {
override fun serialize(data: CallLinkRoomId): String {
return Base64.encodeBytes(data.roomId)

View file

@ -85,9 +85,9 @@
android:focusable="true"
android:theme="@style/Widget.Material3.FloatingActionButton.Secondary"
android:transitionName="camera_fab"
app:shapeAppearanceOverlay="@style/Signal.ShapeOverlay.Rounded.Fab"
app:backgroundTint="@color/signal_colorSurfaceVariant"
app:elevation="0dp"
app:shapeAppearanceOverlay="@style/Signal.ShapeOverlay.Rounded.Fab"
app:srcCompat="@drawable/ic_camera_outline_24"
app:tint="@color/signal_colorOnSurface" />

View file

@ -5975,6 +5975,15 @@
<string name="CallContextMenu__delete">Delete</string>
<!-- Call Log Fragment -->
<!-- Displayed when deleting call history items -->
<string name="CallLogFragment__deleting">Deleting…</string>
<!-- Displayed in a toast when a deletion fails for an unknown reason -->
<string name="CallLogFragment__deletion_failed">Deletion failed.</string>
<!-- Displayed as message in error dialog when can't delete links -->
<plurals name="CallLogFragment__cant_delete_call_link">
<item quantity="one">Can\'t delete link. Check your connection and try again.</item>
<item quantity="other">Not all call links could be deleted. Check your connection and try again.</item>
</plurals>
<!-- Snackbar text after clearing the call history -->
<string name="CallLogFragment__cleared_call_history">Cleared call history</string>
<!-- Dialog title to clear all call events -->