Ensure owned call links are revoked on delete.
This commit is contained in:
parent
03a212eee4
commit
290b0fe46f
12 changed files with 334 additions and 126 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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 -->
|
||||
|
|
Loading…
Add table
Reference in a new issue