Add call tab event grouping.

This commit is contained in:
Alex Hart
2023-04-12 13:22:33 -03:00
committed by Greyson Parrelli
parent fd1ff5e438
commit e8570c3680
11 changed files with 164 additions and 69 deletions

View File

@@ -155,24 +155,29 @@ class CallLogAdapter(
binding.callRecipientAvatar.setAvatar(GlideApp.with(binding.callRecipientAvatar), model.call.peer, true)
binding.callRecipientBadge.setBadgeFromRecipient(model.call.peer)
binding.callRecipientName.text = model.call.peer.getDisplayName(context)
presentCallInfo(model.call.call, model.call.date)
presentCallInfo(model.call, model.call.date)
presentCallType(model)
}
private fun presentCallInfo(call: CallTable.Call, date: Long) {
private fun presentCallInfo(call: CallLogRow.Call, date: Long) {
val callState = context.getString(getCallStateStringRes(call.record))
binding.callInfo.text = context.getString(
R.string.CallLogAdapter__s_dot_s,
context.getString(getCallStateStringRes(call)),
if (call.children.size > 1) {
context.getString(R.string.CallLogAdapter__d_s, call.children.size, callState)
} else {
callState
},
DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), date)
)
binding.callInfo.setRelativeDrawables(
start = getCallStateDrawableRes(call)
start = getCallStateDrawableRes(call.record)
)
val color = ContextCompat.getColor(
context,
if (call.event == CallTable.Event.MISSED) {
if (call.record.event == CallTable.Event.MISSED) {
R.color.signal_colorError
} else {
R.color.signal_colorOnSurface
@@ -188,7 +193,7 @@ class CallLogAdapter(
}
private fun presentCallType(model: CallModel) {
when (model.call.call.type) {
when (model.call.record.type) {
CallTable.Type.AUDIO_CALL -> {
binding.callType.setImageResource(R.drawable.symbol_phone_24)
binding.callType.setOnClickListener { onStartAudioCallClicked(model.call.peer) }

View File

@@ -72,7 +72,7 @@ class CallLogContextMenu(
iconRes = R.drawable.symbol_info_24,
title = fragment.getString(R.string.CallContextMenu__info)
) {
val intent = ConversationSettingsActivity.forCall(fragment.requireContext(), call.peer, longArrayOf(call.call.messageId!!))
val intent = ConversationSettingsActivity.forCall(fragment.requireContext(), call.peer, longArrayOf(call.record.messageId!!))
fragment.startActivity(intent)
}
}
@@ -87,7 +87,7 @@ class CallLogContextMenu(
}
private fun getDeleteActionItem(call: CallLogRow.Call): ActionItem? {
if (call.call.event == CallTable.Event.ONGOING) {
if (call.record.event == CallTable.Event.ONGOING) {
return null
}

View File

@@ -277,7 +277,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
if (viewModel.selectionStateSnapshot.isNotEmpty(binding.recycler.adapter!!.itemCount)) {
viewModel.toggleSelected(callLogRow.id)
} else {
val intent = ConversationSettingsActivity.forCall(requireContext(), callLogRow.peer, longArrayOf(callLogRow.call.messageId!!))
val intent = ConversationSettingsActivity.forCall(requireContext(), callLogRow.peer, (callLogRow.id as CallLogRow.Id.Call).children.toLongArray())
startActivity(intent)
}
}

View File

@@ -36,18 +36,18 @@ class CallLogRepository : CallLogPagedDataSource.CallRepository {
}
fun deleteSelectedCallLogs(
selectedCallIds: Set<Long>
selectedCallRowIds: Set<Long>
): Completable {
return Completable.fromAction {
SignalDatabase.calls.deleteCallEvents(selectedCallIds)
SignalDatabase.calls.deleteCallEvents(selectedCallRowIds)
}.observeOn(Schedulers.io())
}
fun deleteAllCallLogsExcept(
selectedCallIds: Set<Long>
selectedCallRowIds: Set<Long>
): Completable {
return Completable.fromAction {
SignalDatabase.calls.deleteAllCallEventsExcept(selectedCallIds)
SignalDatabase.calls.deleteAllCallEventsExcept(selectedCallRowIds)
}.observeOn(Schedulers.io())
}
}

View File

@@ -15,11 +15,12 @@ sealed class CallLogRow {
* An incoming, outgoing, or missed call.
*/
data class Call(
val call: CallTable.Call,
val record: CallTable.Call,
val peer: Recipient,
val date: Long,
val groupCallState: GroupCallState,
override val id: Id = Id.Call(call.callId)
val children: Set<Long>,
override val id: Id = Id.Call(children)
) : CallLogRow()
/**
@@ -34,7 +35,7 @@ sealed class CallLogRow {
}
sealed class Id {
data class Call(val callId: Long) : Id()
data class Call(val children: Set<Long>) : Id()
object ClearFilter : Id()
object CreateCallLink : Id()
}

View File

@@ -28,15 +28,16 @@ class CallLogStagedDeletion(
}
isCommitted = true
val callIds = stateSnapshot.selected()
val callRowIds = stateSnapshot.selected()
.filterIsInstance<CallLogRow.Id.Call>()
.map { it.callId }
.map { it.children }
.flatten()
.toSet()
if (stateSnapshot.isExclusionary()) {
repository.deleteAllCallLogsExcept(callIds).subscribe()
repository.deleteAllCallLogsExcept(callRowIds).subscribe()
} else {
repository.deleteSelectedCallLogs(callIds).subscribe()
repository.deleteSelectedCallLogs(callRowIds).subscribe()
}
}
}

View File

@@ -40,13 +40,14 @@ class ConversationSettingsRepository(
private val groupManagementRepository: GroupManagementRepository = GroupManagementRepository(context)
) {
fun getCallEvents(callMessageIds: LongArray): Single<List<Pair<CallTable.Call, MessageRecord>>> {
return if (callMessageIds.isEmpty()) {
fun getCallEvents(callRowIds: LongArray): Single<List<Pair<CallTable.Call, MessageRecord>>> {
return if (callRowIds.isEmpty()) {
Single.just(emptyList())
} else {
Single.fromCallable {
val callMap = SignalDatabase.calls.getCalls(callMessageIds.toList())
SignalDatabase.messages.getMessages(callMessageIds.toList()).iterator().asSequence()
val callMap = SignalDatabase.calls.getCallsByRowIds(callRowIds.toList())
val messageIds = callMap.values.mapNotNull { it.messageId }
SignalDatabase.messages.getMessages(messageIds).iterator().asSequence()
.filter { callMap.containsKey(it.id) }
.map { callMap[it.id]!! to it }
.toList()

View File

@@ -8,12 +8,15 @@ import org.signal.core.util.IntSerializer
import org.signal.core.util.Serializer
import org.signal.core.util.SqlUtil
import org.signal.core.util.delete
import org.signal.core.util.flatten
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
import org.signal.core.util.readToList
import org.signal.core.util.readToMap
import org.signal.core.util.readToSingleLong
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
import org.signal.core.util.requireObject
import org.signal.core.util.requireString
import org.signal.core.util.select
@@ -32,6 +35,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallEvent
import java.util.UUID
import java.util.concurrent.TimeUnit
/**
* Contains details for each 1:1 call.
@@ -40,6 +44,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
companion object {
private val TAG = Log.tag(CallTable::class.java)
private val TIME_WINDOW = TimeUnit.HOURS.toMillis(4)
private const val TABLE_NAME = "call"
private const val ID = "_id"
@@ -152,19 +157,37 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
}
fun getCalls(messageIds: Collection<Long>): Map<Long, Call> {
val calls = mutableMapOf<Long, Call>()
val queries = SqlUtil.buildCollectionQuery(MESSAGE_ID, messageIds)
queries.forEach { query ->
val cursor = readableDatabase
val maps = queries.map { query ->
readableDatabase
.select()
.from(TABLE_NAME)
.where("$EVENT != ${Event.serialize(Event.DELETE)} AND ${query.where}", query.whereArgs)
.run()
calls.putAll(cursor.readToList { c -> c.requireLong(MESSAGE_ID) to Call.deserialize(c) })
.readToMap { c -> c.requireLong(MESSAGE_ID) to Call.deserialize(c) }
}
return calls
return maps.flatten()
}
/**
* @param callRowIds The CallTable.ID collection to query
*
* @return a map of raw MessageId -> Call
*/
fun getCallsByRowIds(callRowIds: Collection<Long>): Map<Long, Call> {
val queries = SqlUtil.buildCollectionQuery(ID, callRowIds)
val maps = queries.map { query ->
readableDatabase
.select()
.from(TABLE_NAME)
.where("$EVENT != ${Event.serialize(Event.DELETE)} AND ${query.where}", query.whereArgs)
.run()
.readToMap { c -> c.requireLong(MESSAGE_ID) to Call.deserialize(c) }
}
return maps.flatten()
}
fun getOldestDeletionTimestamp(): Long {
@@ -670,14 +693,14 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
.run()
}
fun deleteCallEvents(callIds: Set<Long>) {
val messageIds = getMessageIds(callIds)
fun deleteCallEvents(callRowIds: Set<Long>) {
val messageIds = getMessageIds(callRowIds)
SignalDatabase.messages.deleteCallUpdates(messageIds)
updateCallEventDeletionTimestamps()
}
fun deleteAllCallEventsExcept(callIds: Set<Long>) {
val messageIds = getMessageIds(callIds)
fun deleteAllCallEventsExcept(callRowIds: Set<Long>) {
val messageIds = getMessageIds(callRowIds)
SignalDatabase.messages.deleteAllCallUpdatesExcept(messageIds)
updateCallEventDeletionTimestamps()
}
@@ -697,10 +720,10 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
}
}
private fun getMessageIds(callIds: Set<Long>): Set<Long> {
private fun getMessageIds(callRowIds: Set<Long>): Set<Long> {
val queries = SqlUtil.buildCollectionQuery(
CALL_ID,
callIds,
ID,
callRowIds,
"$MESSAGE_ID NOT NULL AND"
)
@@ -718,12 +741,12 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
// endregion
private fun getCallsCursor(isCount: Boolean, offset: Int, limit: Int, searchTerm: String?, filter: CallLogFilter): Cursor {
val filterClause = when (filter) {
val filterClause: SqlUtil.Query = when (filter) {
CallLogFilter.ALL -> SqlUtil.buildQuery("$EVENT != ${Event.serialize(Event.DELETE)}")
CallLogFilter.MISSED -> SqlUtil.buildQuery("$EVENT == ${Event.serialize(Event.MISSED)}")
}
val queryClause = if (!searchTerm.isNullOrEmpty()) {
val queryClause: SqlUtil.Query = if (!searchTerm.isNullOrEmpty()) {
val glob = SqlUtil.buildCaseInsensitiveGlobPattern(searchTerm)
val selection =
"""
@@ -740,41 +763,86 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
SqlUtil.buildQuery("")
}
val whereClause = filterClause and queryClause
val where = if (whereClause.where.isNotEmpty()) {
"WHERE ${whereClause.where}"
} else {
""
}
val offsetLimit = if (limit > 0) {
"LIMIT $offset,$limit"
} else {
""
}
val projection = if (isCount) {
"COUNT(*),"
} else {
"p.$ID, $TIMESTAMP, $EVENT, $DIRECTION, $PEER, p.$TYPE, $CALL_ID, $MESSAGE_ID, $RINGER, children, ${MessageTable.DATE_RECEIVED}, ${MessageTable.BODY},"
}
//language=sql
val statement = """
SELECT
${if (isCount) "COUNT(*)," else "$TABLE_NAME.*, ${MessageTable.DATE_RECEIVED}, ${MessageTable.BODY},"}
LOWER(
COALESCE(
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.SYSTEM_JOINED_NAME}, ''),
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.SYSTEM_GIVEN_NAME}, ''),
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.PROFILE_JOINED_NAME}, ''),
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.PROFILE_GIVEN_NAME}, ''),
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.USERNAME}, '')
SELECT $projection
LOWER(
COALESCE(
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.SYSTEM_JOINED_NAME}, ''),
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.SYSTEM_GIVEN_NAME}, ''),
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.PROFILE_JOINED_NAME}, ''),
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.PROFILE_GIVEN_NAME}, ''),
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.USERNAME}, '')
)
) AS sort_name
FROM (
WITH cte AS (
SELECT
$ID, $TIMESTAMP, $EVENT, $DIRECTION, $PEER, $TYPE, $CALL_ID, $MESSAGE_ID, $RINGER,
(
SELECT
$ID
FROM
$TABLE_NAME
WHERE
$TABLE_NAME.$DIRECTION = c.$DIRECTION
AND $TABLE_NAME.$PEER = c.$PEER
AND $TABLE_NAME.$TIMESTAMP - $TIME_WINDOW <= c.$TIMESTAMP
AND $TABLE_NAME.$TIMESTAMP >= c.$TIMESTAMP
ORDER BY
$TIMESTAMP DESC
) as parent,
(
SELECT
group_concat($ID)
FROM
$TABLE_NAME
WHERE
$TABLE_NAME.$DIRECTION = c.$DIRECTION
AND $TABLE_NAME.$PEER = c.$PEER
AND c.$TIMESTAMP - $TIME_WINDOW <= $TABLE_NAME.$TIMESTAMP
AND c.$TIMESTAMP >= $TABLE_NAME.$TIMESTAMP
) as children
FROM
$TABLE_NAME c
WHERE ${filterClause.where}
ORDER BY
$TIMESTAMP DESC
)
) AS sort_name
FROM $TABLE_NAME
INNER JOIN ${RecipientTable.TABLE_NAME} ON ${RecipientTable.TABLE_NAME}.${RecipientTable.ID} = $TABLE_NAME.$PEER
INNER JOIN ${MessageTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = $TABLE_NAME.$MESSAGE_ID
$where
ORDER BY ${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED} DESC
SELECT
*,
CASE
WHEN LAG (parent, 1, 0) OVER (
ORDER BY
$TIMESTAMP DESC
) != parent THEN $ID
ELSE parent
END true_parent
FROM
cte
) p
INNER JOIN ${RecipientTable.TABLE_NAME} ON ${RecipientTable.TABLE_NAME}.${RecipientTable.ID} = $PEER
INNER JOIN ${MessageTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = $MESSAGE_ID
WHERE true_parent = p.$ID ${if (queryClause.where.isNotEmpty()) "AND ${queryClause.where}" else ""}
$offsetLimit
""".trimIndent()
return readableDatabase.query(statement, whereClause.whereArgs)
return readableDatabase.query(
statement,
queryClause.whereArgs
)
}
fun getCallsCount(searchTerm: String?, filter: CallLogFilter): Int {
@@ -785,17 +853,21 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
}
fun getCalls(offset: Int, limit: Int, searchTerm: String?, filter: CallLogFilter): List<CallLogRow.Call> {
return getCallsCursor(false, offset, limit, searchTerm, filter).readToList {
val call = Call.deserialize(it)
return getCallsCursor(false, offset, limit, searchTerm, filter).readToList { cursor ->
val call = Call.deserialize(cursor)
val recipient = Recipient.resolved(call.peer)
val date = it.requireLong(MessageTable.DATE_RECEIVED)
val groupCallDetails = GroupCallUpdateDetailsUtil.parse(it.requireString(MessageTable.BODY))
val date = cursor.requireLong(MessageTable.DATE_RECEIVED)
val groupCallDetails = GroupCallUpdateDetailsUtil.parse(cursor.requireString(MessageTable.BODY))
CallLogRow.Call(
call = call,
record = call,
peer = recipient,
date = date,
groupCallState = CallLogRow.GroupCallState.fromDetails(groupCallDetails)
groupCallState = CallLogRow.GroupCallState.fromDetails(groupCallDetails),
children = cursor.requireNonNullString("children")
.split(',')
.map { it.toLong() }
.toSet()
)
}
}

View File

@@ -5784,6 +5784,8 @@
<string name="CallLogAdapter__join">Join</string>
<!-- Displayed on Group Call button if user is in the call -->
<string name="CallLogAdapter__return">Return</string>
<!-- Call state template when there is more than one call collapsed into a single row. D is a number > 1 and S is a call info string (like Missed) -->
<string name="CallLogAdapter__d_s">(%1$d) %2$s</string>
<!-- Call Log context menu -->
<!-- Displayed as a context menu item to start a video call -->

View File

@@ -0,0 +1,8 @@
package org.signal.core.util
/**
* Flattens a List of Map<K, V> into a Map<K, V> using the + operator.
*
* @return A Map containing all of the K, V pairings of the maps contained in the original list.
*/
fun <K, V> List<Map<K, V>>.flatten(): Map<K, V> = foldRight(emptyMap()) { a, b -> a + b }

View File

@@ -137,6 +137,11 @@ inline fun <T> Cursor.readToList(predicate: (T) -> Boolean = { true }, mapper: (
return list
}
@JvmOverloads
inline fun <K, V> Cursor.readToMap(predicate: (Pair<K, V>) -> Boolean = { true }, mapper: (Cursor) -> Pair<K, V>): Map<K, V> {
return readToList(predicate, mapper).associate { it }
}
inline fun <T> Cursor.readToSet(predicate: (T) -> Boolean = { true }, mapper: (Cursor) -> T): Set<T> {
val set = mutableSetOf<T>()
use {