Implement better handling for call peeking when opening the calls tab.

This commit is contained in:
Alex Hart
2024-05-02 14:25:17 -03:00
parent cd880b0879
commit 55abd88a03
6 changed files with 207 additions and 4 deletions

View File

@@ -122,6 +122,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
initializeSharedElementTransition()
viewLifecycleOwner.lifecycle.addObserver(conversationUpdateTick)
viewLifecycleOwner.lifecycle.addObserver(viewModel.callLogPeekHelper)
val callLogAdapter = CallLogAdapter(this)
disposables.bindTo(viewLifecycleOwner)

View File

@@ -67,6 +67,7 @@ class CallLogPagedDataSource(
callLogRows.add(CallLogRow.ClearFilter)
}
repository.onCallTabPageLoaded(callLogRows)
return callLogRows
}
@@ -83,5 +84,6 @@ class CallLogPagedDataSource(
fun getCalls(query: String?, filter: CallLogFilter, start: Int, length: Int): List<CallLogRow>
fun getCallLinksCount(query: String?, filter: CallLogFilter): Int
fun getCallLinks(query: String?, filter: CallLogFilter, start: Int, length: Int): List<CallLogRow>
fun onCallTabPageLoaded(pageData: List<CallLogRow>)
}
}

View File

@@ -0,0 +1,188 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.calls.log
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.util.ThrottledDebouncer
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor
import java.util.concurrent.Executor
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds
/**
* Peeks calls in the call log as data is loaded in, according to
* an algorithm.
*/
class CallLogPeekHelper : DefaultLifecycleObserver {
companion object {
private val TAG = Log.tag(CallLogPeekHelper::class.java)
private const val PEEK_SIZE = 10
}
private val executor: Executor = SerialExecutor(SignalExecutors.BOUNDED_IO)
private val debouncer = ThrottledDebouncer(30.seconds.inWholeMilliseconds)
private val dataSet = mutableSetOf<PeekEntry>()
private val peekQueue = mutableSetOf<PeekEntry>()
private var isFirstLoad = true
private var isPaused = false
override fun onResume(owner: LifecycleOwner) {
executor.execute {
isPaused = false
performPeeks()
}
}
override fun onPause(owner: LifecycleOwner) {
executor.execute {
isPaused = true
debouncer.clear()
}
}
/**
* Called whenever the underlying datasource has been invalidated.
*/
fun onDataSetInvalidated() {
executor.execute {
debouncer.clear()
dataSet.clear()
peekQueue.clear()
}
}
/**
* Called whenever a new page of data is loaded by the datasource.
*/
fun onPageLoaded(pageData: List<CallLogRow>) {
executor.execute {
handleActiveCallLinks(pageData)
handleActiveGroupCalls(pageData)
handleInactiveGroupCalls(pageData)
handleInactiveCallLinks(pageData)
performPeeks()
}
}
/**
* Adds any and all active call links to our data set and queue
*/
private fun handleActiveCallLinks(pageData: List<CallLogRow>) {
val activeUnusedCallLinks: List<PeekEntry> = pageData.filterIsInstance<CallLogRow.CallLink>()
.filter { it.callLinkPeekInfo?.isActive == true }
.map { PeekEntry(it.recipient.id, PeekEntryIdentifier.CallLink(it.record.roomId, PeekEntryType.CALL_LINK)) }
val activeCallLinksFromEvents: List<PeekEntry> = pageData.filterIsInstance<CallLogRow.Call>()
.filter { it.peer.isCallLink && it.callLinkPeekInfo?.isActive == true }
.map { PeekEntry(it.peer.id, PeekEntryIdentifier.Call(it.record.callId, PeekEntryType.CALL_LINK)) }
val activeCallLinks: List<PeekEntry> = activeUnusedCallLinks + activeCallLinksFromEvents
dataSet.addAll(activeCallLinks)
peekQueue.addAll(activeCallLinks)
}
/**
* Adds any and all active group calls to our dataset and queue.
*/
private fun handleActiveGroupCalls(pageData: List<CallLogRow>) {
val activeGroupCalls: List<PeekEntry> = pageData.filterIsInstance<CallLogRow.Call>()
.filter { it.peer.isGroup && it.groupCallState != CallLogRow.GroupCallState.NONE }
.map { PeekEntry(it.peer.id, PeekEntryIdentifier.Call(it.record.callId, PeekEntryType.GROUP_CALL)) }
dataSet.addAll(activeGroupCalls)
peekQueue.addAll(activeGroupCalls)
}
/**
* Removes any and all inactive group calls from our dataset and queue.
*/
private fun handleInactiveGroupCalls(pageData: List<CallLogRow>) {
val inactiveGroupCalls: Set<PeekEntry> = pageData.filterIsInstance<CallLogRow.Call>()
.filter { it.peer.isGroup && it.groupCallState == CallLogRow.GroupCallState.NONE }
.map { PeekEntry(it.peer.id, PeekEntryIdentifier.Call(it.record.callId, PeekEntryType.GROUP_CALL)) }
.toSet()
peekQueue.removeAll(inactiveGroupCalls)
dataSet.removeAll(inactiveGroupCalls)
}
/**
* On first load, adds all inactive call links to our queue. On subsequent calls, removes them from the dataset.
*/
private fun handleInactiveCallLinks(pageData: List<CallLogRow>) {
val inactiveUnusedCallLinks: List<PeekEntry> = pageData.filterIsInstance<CallLogRow.CallLink>()
.filter { it.callLinkPeekInfo?.isActive != true }
.map { PeekEntry(it.recipient.id, PeekEntryIdentifier.CallLink(it.record.roomId, PeekEntryType.CALL_LINK)) }
val inactiveCallLinksFromEvents: List<PeekEntry> = pageData.filterIsInstance<CallLogRow.Call>()
.filter { it.callLinkPeekInfo?.isActive != true }
.filter { it.record.timestamp <= 10.days.inWholeMilliseconds }
.map { PeekEntry(it.peer.id, PeekEntryIdentifier.Call(it.record.callId, PeekEntryType.CALL_LINK)) }
val inactiveCallLinks: Set<PeekEntry> = (inactiveUnusedCallLinks + inactiveCallLinksFromEvents).take(10).toSet()
if (isFirstLoad) {
isFirstLoad = false
peekQueue.addAll(inactiveCallLinks)
} else {
dataSet.removeAll(inactiveCallLinks)
}
}
private fun performPeeks() {
executor.execute {
if (peekQueue.isEmpty() || isPaused) {
return@execute
}
Log.d(TAG, "Peeks in queue. Taking first $PEEK_SIZE.")
val items = peekQueue.take(PEEK_SIZE)
val remaining = peekQueue.drop(PEEK_SIZE)
peekQueue.clear()
peekQueue.addAll(remaining)
items.forEach {
when (it.identifier.peekEntryType) {
PeekEntryType.CALL_LINK -> ApplicationDependencies.getSignalCallManager().peekCallLinkCall(it.recipientId)
PeekEntryType.GROUP_CALL -> ApplicationDependencies.getSignalCallManager().peekGroupCall(it.recipientId)
}
}
Log.d(TAG, "Began peeks for ${items.size} calls.")
peekQueue.addAll(dataSet)
debouncer.publish { performPeeks() }
}
}
private enum class PeekEntryType {
CALL_LINK,
GROUP_CALL
}
private sealed interface PeekEntryIdentifier {
val peekEntryType: PeekEntryType
data class CallLink(private val roomId: CallLinkRoomId, override val peekEntryType: PeekEntryType = PeekEntryType.CALL_LINK) : PeekEntryIdentifier
data class Call(private val callId: Long, override val peekEntryType: PeekEntryType) : PeekEntryIdentifier
}
private data class PeekEntry(
val recipientId: RecipientId,
val identifier: PeekEntryIdentifier
)
}

View File

@@ -17,7 +17,8 @@ import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
class CallLogRepository(
private val updateCallLinkRepository: UpdateCallLinkRepository = UpdateCallLinkRepository()
private val updateCallLinkRepository: UpdateCallLinkRepository = UpdateCallLinkRepository(),
private val callLogPeekHelper: CallLogPeekHelper
) : CallLogPagedDataSource.CallRepository {
override fun getCallsCount(query: String?, filter: CallLogFilter): Int {
return SignalDatabase.calls.getCallsCount(query, filter)
@@ -41,6 +42,12 @@ class CallLogRepository(
}
}
override fun onCallTabPageLoaded(pageData: List<CallLogRow>) {
SignalExecutors.BOUNDED_IO.execute {
callLogPeekHelper.onPageLoaded(pageData)
}
}
fun markAllCallEventsRead() {
SignalExecutors.BOUNDED_IO.execute {
val latestCall = SignalDatabase.calls.getLatestCall() ?: return@execute

View File

@@ -24,7 +24,8 @@ import java.util.concurrent.TimeUnit
* ViewModel for call log management.
*/
class CallLogViewModel(
private val callLogRepository: CallLogRepository = CallLogRepository()
val callLogPeekHelper: CallLogPeekHelper = CallLogPeekHelper(),
private val callLogRepository: CallLogRepository = CallLogRepository(callLogPeekHelper = callLogPeekHelper)
) : ViewModel() {
private val callLogStore = RxStore(CallLogState())
@@ -77,6 +78,7 @@ class CallLogViewModel(
}
disposables += callLogRepository.listenForChanges().subscribe {
callLogPeekHelper.onDataSetInvalidated()
controller.onDataInvalidated()
}
@@ -92,6 +94,7 @@ class CallLogViewModel(
.observeOn(Schedulers.computation())
.distinctUntilChanged()
.subscribe {
callLogPeekHelper.onDataSetInvalidated()
controller.onDataInvalidated()
}
}

View File

@@ -12,13 +12,15 @@ import org.signal.ringrtc.PeekInfo
* App-level peek info object for call links.
*/
data class CallLinkPeekInfo(
val callId: CallId?
val callId: CallId?,
val isActive: Boolean
) {
companion object {
@JvmStatic
fun fromPeekInfo(peekInfo: PeekInfo): CallLinkPeekInfo {
return CallLinkPeekInfo(
callId = peekInfo.eraId?.let { CallId.fromEra(it) }
callId = peekInfo.eraId?.let { CallId.fromEra(it) },
isActive = peekInfo.joinedMembers.isNotEmpty()
)
}
}