mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 20:48:43 +00:00
Implement better handling for call peeking when opening the calls tab.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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>)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user