Add proper call tab return state.

This commit is contained in:
Alex Hart
2024-09-06 13:25:28 -03:00
committed by Cody Henthorne
parent 514f7cc767
commit 1f09f48e6b
13 changed files with 171 additions and 99 deletions

View File

@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.calls.links
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -33,20 +34,21 @@ import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.signal.core.ui.Buttons
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.CallLinkPeekInfo
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 java.time.Instant
@Preview
@SignalPreview
@Composable
private fun SignalCallRowPreview() {
val callLink = remember {
@@ -63,17 +65,29 @@ private fun SignalCallRowPreview() {
)
)
}
SignalTheme(false) {
SignalCallRow(
callLink = callLink,
onJoinClicked = {}
)
Previews.Preview {
Column(
verticalArrangement = spacedBy(8.dp)
) {
SignalCallRow(
callLink = callLink,
callLinkPeekInfo = null,
onJoinClicked = {}
)
SignalCallRow(
callLink = callLink,
callLinkPeekInfo = CallLinkPeekInfo(null, true, true),
onJoinClicked = {}
)
}
}
}
@Composable
fun SignalCallRow(
callLink: CallLinkTable.CallLink,
callLinkPeekInfo: CallLinkPeekInfo?,
onJoinClicked: (() -> Unit)?,
modifier: Modifier = Modifier
) {
@@ -140,7 +154,13 @@ fun SignalCallRow(
),
modifier = Modifier.align(CenterVertically)
) {
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__join))
val textId = if (callLinkPeekInfo?.isJoined == true) {
R.string.CallLogAdapter__return
} else {
R.string.CreateCallLinkBottomSheetDialogFragment__join
}
Text(text = stringResource(id = textId))
}
}
}

View File

@@ -100,6 +100,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
SignalCallRow(
callLink = callLink,
callLinkPeekInfo = null,
onJoinClicked = this@CreateCallLinkBottomSheetDialogFragment::onJoinClicked
)

View File

@@ -41,7 +41,6 @@ import org.thoughtcrime.securesms.calls.links.CallLinks
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
import org.thoughtcrime.securesms.calls.links.SignalCallRow
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
@@ -200,12 +199,11 @@ private interface CallLinkDetailsCallback {
@Preview
@Composable
private fun CallLinkDetailsPreview() {
val avatarColor = remember {
AvatarColor.random()
}
val callLink = remember {
val credentials = CallLinkCredentials.generate()
val credentials = CallLinkCredentials(
byteArrayOf(1, 2, 3, 4),
byteArrayOf(3, 4, 5, 6)
)
CallLinkTable.CallLink(
recipientId = RecipientId.UNKNOWN,
roomId = credentials.roomId,
@@ -258,6 +256,7 @@ private fun CallLinkDetails(
Column(modifier = Modifier.padding(paddingValues)) {
SignalCallRow(
callLink = state.callLink,
callLinkPeekInfo = state.peekInfo,
onJoinClicked = callback::onJoinClicked,
modifier = Modifier.padding(top = 16.dp, bottom = 12.dp)
)

View File

@@ -7,9 +7,11 @@ package org.thoughtcrime.securesms.calls.links.details
import androidx.compose.runtime.Immutable
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.service.webrtc.CallLinkPeekInfo
@Immutable
data class CallLinkDetailsState(
val displayRevocationDialog: Boolean = false,
val callLink: CallLinkTable.CallLink? = null
val callLink: CallLinkTable.CallLink? = null,
val peekInfo: CallLinkPeekInfo? = null
)

View File

@@ -10,14 +10,17 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.BehaviorSubject
import org.signal.ringrtc.CallLinkState
import org.thoughtcrime.securesms.calls.links.CallLinks
import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
@@ -43,13 +46,33 @@ class CallLinkDetailsViewModel(
init {
disposables += repository.refreshCallLinkState(callLinkRoomId)
disposables += CallLinks.watchCallLink(callLinkRoomId).subscribeBy {
_state.value = _state.value.copy(callLink = it)
}
disposables += CallLinks.watchCallLink(callLinkRoomId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
_state.value = _state.value.copy(callLink = it)
}
disposables += repository
.watchCallLinkRecipient(callLinkRoomId)
.subscribeBy(onNext = recipientSubject::onNext)
disposables += recipientSubject
.map { it.id }
.distinctUntilChanged()
.flatMap { recipientId ->
AppDependencies.signalCallManager.peekInfoCache
.distinctUntilChanged()
.filter { it.containsKey(recipientId) }
.map { it[recipientId]!! }
.distinctUntilChanged()
.toObservable()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy { callLinkPeekInfo ->
_state.value = _state.value.copy(peekInfo = callLinkPeekInfo)
}
}
override fun onCleared() {

View File

@@ -228,13 +228,28 @@ class CallLogAdapter(
)
)
binding.callType.setImageResource(R.drawable.symbol_video_24)
binding.callType.contentDescription = context.getString(R.string.CallLogAdapter__start_a_video_call)
binding.callType.setOnClickListener {
onStartVideoCallClicked(model.callLink.recipient, true)
if (model.callLink.callLinkPeekInfo?.isActive == true) {
binding.groupCallButton.setText(
if (model.callLink.callLinkPeekInfo.isJoined) {
R.string.CallLogAdapter__return
} else {
R.string.CallLogAdapter__join
}
)
binding.groupCallButton.setOnClickListener {
onStartVideoCallClicked(model.callLink.recipient, true)
}
binding.callType.visible = false
binding.groupCallButton.visible = true
} else {
binding.callType.setImageResource(R.drawable.symbol_video_24)
binding.callType.contentDescription = context.getString(R.string.CallLogAdapter__start_a_video_call)
binding.callType.setOnClickListener {
onStartVideoCallClicked(model.callLink.recipient, true)
}
binding.callType.visible = true
binding.groupCallButton.visible = false
}
binding.callType.visible = true
binding.groupCallButton.visible = false
}
}
@@ -338,7 +353,30 @@ class CallLogAdapter(
binding.groupCallButton.visible = false
}
CallTable.Type.GROUP_CALL, CallTable.Type.AD_HOC_CALL -> {
CallTable.Type.AD_HOC_CALL -> {
binding.callType.setImageResource(R.drawable.symbol_video_24)
binding.callType.contentDescription = context.getString(R.string.CallLogAdapter__start_a_video_call)
binding.callType.setOnClickListener { onStartVideoCallClicked(model.call.peer, model.call.canUserBeginCall) }
binding.groupCallButton.setOnClickListener { onStartVideoCallClicked(model.call.peer, model.call.canUserBeginCall) }
if (model.call.callLinkPeekInfo?.isActive == true) {
binding.callType.visible = false
binding.groupCallButton.visible = true
binding.groupCallButton.setText(
if (model.call.callLinkPeekInfo.isJoined) {
R.string.CallLogAdapter__return
} else {
R.string.CallLogAdapter__join
}
)
} else {
binding.callType.visible = true
binding.groupCallButton.visible = false
}
}
CallTable.Type.GROUP_CALL -> {
binding.callType.setImageResource(R.drawable.symbol_video_24)
binding.callType.contentDescription = context.getString(R.string.CallLogAdapter__start_a_video_call)
binding.callType.setOnClickListener { onStartVideoCallClicked(model.call.peer, model.call.canUserBeginCall) }
@@ -401,6 +439,7 @@ class CallLogAdapter(
call.direction == CallTable.Direction.OUTGOING -> R.string.CallLogAdapter__outgoing
else -> throw AssertionError()
}
else -> if (call.isDisplayedAsMissedCallInUi) R.string.CallLogAdapter__missed else R.string.CallLogAdapter__incoming
}
}

View File

@@ -11,7 +11,6 @@ import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.CallLinkPeekJob
import org.thoughtcrime.securesms.jobs.CallLogEventSendJob
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
@@ -158,29 +157,4 @@ class CallLogRepository(
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}
}
fun peekCallLinks(): Completable {
return Completable.fromAction {
val callLinks: List<CallLogRow.CallLink> = SignalDatabase.callLinks.getCallLinks(
query = null,
offset = 0,
limit = 10
)
val callEvents: List<CallLogRow.Call> = SignalDatabase.calls.getCalls(
offset = 0,
limit = 10,
searchTerm = null,
filter = CallLogFilter.AD_HOC
)
val recipients = (callLinks.map { it.recipient } + callEvents.map { it.peer }).toSet()
val jobs = recipients.take(10).map {
CallLinkPeekJob(it.id)
}
AppDependencies.jobManager.addAll(jobs)
}.subscribeOn(Schedulers.io())
}
}

View File

@@ -6,7 +6,6 @@ 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
import io.reactivex.rxjava3.processors.BehaviorProcessor
@@ -16,9 +15,7 @@ import org.signal.paging.PagedData
import org.signal.paging.PagingConfig
import org.signal.paging.ProxyPagingController
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.rx.RxStore
import java.util.concurrent.TimeUnit
/**
* ViewModel for call log management.
@@ -82,22 +79,15 @@ class CallLogViewModel(
controller.onDataInvalidated()
}
if (RemoteConfig.adHocCalling) {
disposables += Observable
.interval(30, TimeUnit.SECONDS, Schedulers.computation())
.flatMapCompletable { callLogRepository.peekCallLinks() }
.subscribe()
disposables += AppDependencies
.signalCallManager
.peekInfoCache
.observeOn(Schedulers.computation())
.distinctUntilChanged()
.subscribe {
callLogPeekHelper.onDataSetInvalidated()
controller.onDataInvalidated()
}
}
disposables += AppDependencies
.signalCallManager
.peekInfoCache
.observeOn(Schedulers.computation())
.distinctUntilChanged()
.subscribe {
callLogPeekHelper.onDataSetInvalidated()
controller.onDataInvalidated()
}
}
override fun onCleared() {

View File

@@ -602,7 +602,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
timestamp: Long,
eraId: String
): Boolean {
return handleCallLinkUpdate(callRecipient, timestamp, CallId.fromEra(eraId), Direction.INCOMING)
return handleCallLinkUpdate(callRecipient, timestamp, CallId.fromEra(eraId), Direction.INCOMING, skipTimestampUpdate = true)
}
fun insertOrUpdateGroupCallFromLocalEvent(
@@ -707,10 +707,16 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
.run()
if (exists && !skipTimestampUpdate) {
db.update(TABLE_NAME)
val updated = db.update(TABLE_NAME)
.values(TIMESTAMP to timestamp)
.where("$PEER = ? AND $CALL_ID = ? AND $TIMESTAMP < ?", callLinkRecipient.id.serialize(), callId.longValue(), timestamp)
.run()
.run() > 0
if (updated) {
Log.d(TAG, "Updated call event for call link. Call Id: $callId")
AppDependencies.databaseObserver.notifyCallUpdateObservers()
}
false
} else if (!exists) {
db.insertInto(TABLE_NAME)
@@ -726,11 +732,12 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
).run(SQLiteDatabase.CONFLICT_ABORT)
Log.d(TAG, "Inserted new call event for call link. Call Id: $callId")
AppDependencies.databaseObserver.notifyCallUpdateObservers()
true
} else false
}
AppDependencies.databaseObserver.notifyCallUpdateObservers()
return didInsert
}
@@ -793,7 +800,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
peekGroupCallEraId: String?,
peekJoinedUuids: Collection<UUID>,
isCallFull: Boolean
): Boolean {
) {
val callId = peekGroupCallEraId?.let { CallId.fromEra(it) }
val recipientId = SignalDatabase.threads.getRecipientIdForThreadId(threadId)
val call = if (callId != null && recipientId != null) {
@@ -802,7 +809,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
null
}
val sameEraId = SignalDatabase.messages.updatePreviousGroupCall(
SignalDatabase.messages.updatePreviousGroupCall(
threadId = threadId,
peekGroupCallEraId = peekGroupCallEraId,
peekJoinedUuids = peekJoinedUuids,
@@ -812,10 +819,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
if (call != null) {
updateGroupCallState(call, peekJoinedUuids)
AppDependencies.databaseObserver.notifyCallUpdateObservers()
}
AppDependencies.databaseObserver.notifyCallUpdateObservers()
return sameEraId
}
fun insertOrUpdateGroupCallFromRingState(
@@ -846,33 +851,43 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
return call.event != Event.RINGING && call.event != Event.GENERIC_GROUP_CALL
}
/**
* @return whether or not a change is detected.
*/
private fun updateGroupCallState(
call: Call,
peekJoinedUuids: Collection<UUID>
) {
updateGroupCallState(
): Boolean {
return updateGroupCallState(
call,
peekJoinedUuids.contains(Recipient.self().requireServiceId().rawUuid),
peekJoinedUuids.isNotEmpty()
)
}
/**
* @return Whether or not a change was detected
*/
private fun updateGroupCallState(
call: Call,
hasLocalUserJoined: Boolean,
isGroupCallActive: Boolean
) {
writableDatabase.update(TABLE_NAME)
): Boolean {
val localJoined = call.didLocalUserJoin || hasLocalUserJoined
return writableDatabase.update(TABLE_NAME)
.values(
LOCAL_JOINED to (call.didLocalUserJoin || hasLocalUserJoined),
LOCAL_JOINED to localJoined,
GROUP_CALL_ACTIVE to isGroupCallActive
)
.where(
"$CALL_ID = ? AND $PEER = ?",
"$CALL_ID = ? AND $PEER = ? AND ($LOCAL_JOINED != ? OR $GROUP_CALL_ACTIVE != ?)",
call.callId,
call.peer.toLong()
call.peer.toLong(),
localJoined.toInt(),
isGroupCallActive.toInt()
)
.run()
.run() > 0
}
private fun handleGroupRingState(

View File

@@ -1022,8 +1022,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
peekJoinedUuids: Collection<UUID>,
isCallFull: Boolean,
isRingingOnLocalDevice: Boolean
): Boolean {
return writableDatabase.withinTransaction { db ->
) {
writableDatabase.withinTransaction { db ->
val cursor = db
.select(*MMS_PROJECTION)
.from(TABLE_NAME)
@@ -1058,8 +1058,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
if (updated) {
notifyConversationListeners(threadId)
}
sameEraId
}
}
}

View File

@@ -12,6 +12,7 @@ import org.signal.ringrtc.PeekInfo
import org.thoughtcrime.securesms.components.webrtc.CallLinkProfileKeySender
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.events.CallParticipant
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -58,6 +59,9 @@ class CallLinkConnectedActionProcessor(
Log.i(tag, "Updating pending list with ${peekInfo.pendingUsers.size} entries.")
val pendingParticipants: List<Recipient> = peekInfo.pendingUsers.map { Recipient.externalPush(ServiceId.ACI.from(it)) }
Log.i(tag, "Storing peek-info in in-memory cache.")
AppDependencies.signalCallManager.emitCallLinkPeekInfoUpdate(callLink.recipientId, peekInfo)
return superState.builder()
.changeCallInfoState()
.setCallLinkPendingParticipants(pendingParticipants)

View File

@@ -7,20 +7,23 @@ package org.thoughtcrime.securesms.service.webrtc
import org.signal.ringrtc.CallId
import org.signal.ringrtc.PeekInfo
import org.thoughtcrime.securesms.recipients.Recipient
/**
* App-level peek info object for call links.
*/
data class CallLinkPeekInfo(
val callId: CallId?,
val isActive: Boolean
val isActive: Boolean,
val isJoined: Boolean
) {
companion object {
@JvmStatic
fun fromPeekInfo(peekInfo: PeekInfo): CallLinkPeekInfo {
return CallLinkPeekInfo(
callId = peekInfo.eraId?.let { CallId.fromEra(it) },
isActive = peekInfo.joinedMembers.isNotEmpty()
isActive = peekInfo.joinedMembers.isNotEmpty(),
isJoined = peekInfo.joinedMembers.contains(Recipient.self().requireServiceId().rawUuid)
)
}
}

View File

@@ -435,11 +435,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
}
}
linkPeekInfoStore.update(store -> {
Map<RecipientId, CallLinkPeekInfo> newHashMap = new HashMap<>(store);
newHashMap.put(id, CallLinkPeekInfo.fromPeekInfo(info));
return newHashMap;
});
emitCallLinkPeekInfoUpdate(id, info);
});
} catch (CallException | VerificationFailedException | InvalidInputException | IOException e) {
Log.i(TAG, "error peeking call link", e);
@@ -447,6 +443,14 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
});
}
public void emitCallLinkPeekInfoUpdate(@NonNull RecipientId recipientId, @NonNull PeekInfo peekInfo) {
linkPeekInfoStore.update(store -> {
Map<RecipientId, CallLinkPeekInfo> newHashMap = new HashMap<>(store);
newHashMap.put(recipientId, CallLinkPeekInfo.fromPeekInfo(peekInfo));
return newHashMap;
});
}
public void peekGroupCall(@NonNull RecipientId id) {
peekGroupCall(id, null);
}