Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ dependencies {
implementation("com.github.nextcloud.android-common:ui:0.34.0")
implementation("com.github.nextcloud.android-common:core:0.34.0")
implementation("com.github.nextcloud-deps:android-talk-webrtc:155.8059.1")
implementation("com.github.nextcloud-releases:talk-clients-vodozemac:0.0.2")

"gplayImplementation"("com.google.android.gms:play-services-base:18.11.1")
"gplayImplementation"("com.google.firebase:firebase-messaging:26.0.0")
Expand Down
172 changes: 118 additions & 54 deletions app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.LaunchedEffect
Expand Down Expand Up @@ -76,6 +77,8 @@ import com.nextcloud.talk.call.MediaConstraintsHelper
import com.nextcloud.talk.call.MessageSender
import com.nextcloud.talk.call.MessageSenderMcu
import com.nextcloud.talk.call.MessageSenderNoMcu
import com.nextcloud.talk.call.e2ee.CallEncryption
import com.nextcloud.talk.call.e2ee.FrameKeyRing
import com.nextcloud.talk.call.MutableLocalCallParticipantModel
import com.nextcloud.talk.call.ReactionAnimator
import com.nextcloud.talk.call.components.ParticipantGrid
Expand Down Expand Up @@ -322,6 +325,7 @@ class CallActivity : CallBaseActivity() {
private var roomJoinRefreshes = 0
private var hasMCU = false
private var hasExternalSignalingServer = false
private var isCallEndToEndEncryptionEnabled = false
private var conversationPassword: String? = null
private var powerManagerUtils: PowerManagerUtils? = null

Expand Down Expand Up @@ -524,10 +528,8 @@ class CallActivity : CallBaseActivity() {
return
}
processExtras(intent.extras!!)

if (warnAndFinishIfCallEndToEndEncryptionUnsupported()) {
return
}
isCallEndToEndEncryptionEnabled =
CapabilitiesUtil.isCallEndToEndEncryptionEnabled(conversationUser.capabilities?.spreedCapability)
Comment thread
AndyScherzinger marked this conversation as resolved.

credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
if (TextUtils.isEmpty(baseUrl)) {
Expand Down Expand Up @@ -570,15 +572,58 @@ class CallActivity : CallBaseActivity() {
checkInitialDevicePermissions()
}

private fun warnAndFinishIfCallEndToEndEncryptionUnsupported(): Boolean {
if (!CapabilitiesUtil.isCallEndToEndEncryptionEnabled(conversationUser?.capabilities?.spreedCapability)) {
/**
* Ends the call when it has to be end-to-end encrypted but can not be; media is never sent unencrypted instead.
*/
private fun failCallEncryption(@StringRes reason: Int) {
if (currentCallStatus === CallStatus.LEAVING) {
return
}
Log.e(TAG, "Ending call: ${context.getString(reason)}")
runOnUiThread { Toast.makeText(context, reason, Toast.LENGTH_LONG).show() }
hangup(shutDownView = true, endCallForAll = false)
}

/**
* The key exchange of the call, null when calls are not end-to-end encrypted or it is not set up yet.
*/
private fun callEncryption(): CallEncryption? =
if (isCallEndToEndEncryptionEnabled) webSocketClient?.callEncryption else null

// Encrypted calls only work through the MCU, which is the only place with a key exchange
private fun ensureMcuForCallEncryption(): Boolean {
if (isCallEndToEndEncryptionEnabled && !hasMCU) {
failCallEncryption(R.string.nc_call_e2ee_requires_hpb)
return false
}
return true
}

// The key exchange is created when the room is joined, so it has to exist before publishing
private fun ensureCallEncryption(): Boolean {
if (isCallEndToEndEncryptionEnabled && callEncryption() == null) {
failCallEncryption(R.string.nc_call_e2ee_setup_failed)
return false
}
Toast.makeText(context, R.string.nc_call_e2ee_not_supported, Toast.LENGTH_LONG).show()
finish()
return true
}

private fun createMessageSender(): MessageSender =
if (hasMCU) {
MessageSenderMcu(
signalingMessageSender,
getParticipantSessionKeys(),
peerConnectionWrapperList,
webSocketClient!!.sessionId
)
} else {
MessageSenderNoMcu(
signalingMessageSender,
getParticipantSessionKeys(),
peerConnectionWrapperList
)
}

private fun initCallRecordingViewModel(recordingState: Int) {
callRecordingViewModel = ViewModelProvider(this, viewModelFactory).get(
CallRecordingViewModel::class.java
Expand Down Expand Up @@ -1717,8 +1762,16 @@ class CallActivity : CallBaseActivity() {

override fun onNext(capabilitiesOverall: CapabilitiesOverall) {
// FIXME check for compatible Call API version
// The policy comes from these fresh capabilities, not from the cached ones of the user
capabilitiesOverall.ocs?.data?.capabilities?.let {
conversationUser.capabilities = it
isCallEndToEndEncryptionEnabled =
CapabilitiesUtil.isCallEndToEndEncryptionEnabled(it.spreedCapability)
}
if (hasExternalSignalingServer) {
setupAndInitiateWebSocketsConnection()
} else if (isCallEndToEndEncryptionEnabled) {
failCallEncryption(R.string.nc_call_e2ee_requires_hpb)
} else {
signalingMessageReceiver = internalSignalingMessageReceiver
signalingMessageReceiver!!.addListener(localParticipantMessageListener)
Expand Down Expand Up @@ -2060,25 +2113,16 @@ class CallActivity : CallBaseActivity() {
hasMCU = webSocketClient!!.hasMCU()
Log.d(TAG, "hasMCU is $hasMCU")

if (hasMCU) {
messageSender = MessageSenderMcu(
signalingMessageSender,
getParticipantSessionKeys(),
peerConnectionWrapperList,
webSocketClient!!.sessionId
)
} else {
messageSender = MessageSenderNoMcu(
signalingMessageSender,
getParticipantSessionKeys(),
peerConnectionWrapperList
)
}
messageSender = createMessageSender()
Comment thread
AndyScherzinger marked this conversation as resolved.
} else {
if (webSocketClient!!.isConnected && currentCallStatus === CallStatus.PUBLISHER_FAILED) {
webSocketClient!!.restartWebSocket()
}
}
// An already connected signaling server sends no new "hello", so the MCU requirement is checked here
if (webSocketClient!!.isConnected && !ensureMcuForCallEncryption()) {
return
}
joinRoomAndCall()
}

Expand All @@ -2104,21 +2148,12 @@ class CallActivity : CallBaseActivity() {
hasMCU = webSocketClient!!.hasMCU()
Log.d(TAG, "hasMCU is $hasMCU")

if (hasMCU) {
messageSender = MessageSenderMcu(
signalingMessageSender,
getParticipantSessionKeys(),
peerConnectionWrapperList,
webSocketClient!!.sessionId
)
} else {
messageSender = MessageSenderNoMcu(
signalingMessageSender,
getParticipantSessionKeys(),
peerConnectionWrapperList
)
if (!ensureMcuForCallEncryption()) {
return
}

messageSender = createMessageSender()

if (!webSocketCommunicationEvent.getHashMap()!!.containsKey("oldResumeId")) {
if (currentCallStatus === CallStatus.RECONNECTING) {
hangup(false, false)
Expand Down Expand Up @@ -2501,7 +2536,7 @@ class CallActivity : CallBaseActivity() {
removeSessions(participantsInCall)
return
}
if (currentCallStatus === CallStatus.LEAVING) {
if (currentCallStatus === CallStatus.LEAVING || (hasMCU && !ensureCallEncryption())) {
return
}
if (hasMCU) {
Expand Down Expand Up @@ -2537,6 +2572,9 @@ class CallActivity : CallBaseActivity() {
var selfJoined = false
val selfParticipantHasAudioOrVideo = participantInCallFlagsHaveAudioOrVideo(selfParticipant)
for (participant in joined) {
if (currentCallStatus === CallStatus.LEAVING) {
return
}
val sessionId = participant.sessionId
if (sessionId == null) {
Log.w(TAG, "Null sessionId for call participant, this should not happen: $participant")
Expand Down Expand Up @@ -2653,6 +2691,7 @@ class CallActivity : CallBaseActivity() {
return null
}
peerConnectionWrapper = createPeerConnectionWrapperForSessionIdAndType(publisher, sessionId, type)
?: return null
synchronized(remoteAudioPlayoutLock) {
peerConnectionWrapperList.add(peerConnectionWrapper)
peerConnectionWrapper.setRemoteAudioPlayoutEnabled(remoteAudioPlayoutEnabled)
Expand Down Expand Up @@ -2680,7 +2719,7 @@ class CallActivity : CallBaseActivity() {
publisher: Boolean,
sessionId: String?,
type: String
): PeerConnectionWrapper {
): PeerConnectionWrapper? {
fun getPeerConnectionFactory(type: String): PeerConnectionFactory? {
fun initScreenSharePeerConnectionFactory(): PeerConnectionFactory? {
val options = PeerConnectionFactory.Options()
Expand Down Expand Up @@ -2739,23 +2778,48 @@ class CallActivity : CallBaseActivity() {
}
}

return PeerConnectionWrapper(
tempPeerConnectionFactory,
iceServers,
tempSdpConstraints,
sessionId,
// #6169: the remote sessionId is a signaling-layer session ID when external signaling
// is used, so the offerer election must compare same-namespace IDs. Pass the local
// signaling session (matching the currentSessionId resolution used elsewhere); with
// internal signaling webSocketClient == null, so callSession is used as before.
if (webSocketClient != null) webSocketClient!!.sessionId else callSession,
tempLocalStream,
tempIsMCUPublisher,
tempHasMCU,
type,
signalingMessageReceiver,
signalingMessageSender
)
val (senderKeyRing, receiverKeyRing) = keyRingsFor(tempIsMCUPublisher, tempHasMCU, sessionId)

return try {
PeerConnectionWrapper(
tempPeerConnectionFactory,
iceServers,
tempSdpConstraints,
sessionId,
// #6169: the remote sessionId is a signaling-layer session ID when external signaling
// is used, so the offerer election must compare same-namespace IDs. Pass the local
// signaling session (matching the currentSessionId resolution used elsewhere); with
// internal signaling webSocketClient == null, so callSession is used as before.
if (webSocketClient != null) webSocketClient!!.sessionId else callSession,
tempLocalStream,
tempIsMCUPublisher,
tempHasMCU,
type,
signalingMessageReceiver,
signalingMessageSender,
isCallEndToEndEncryptionEnabled,
senderKeyRing,
receiverKeyRing
)
} catch (e: IllegalStateException) {
Log.e(TAG, "Not creating a connection to $sessionId", e)
failCallEncryption(R.string.nc_call_e2ee_setup_failed)
null
}
}

// The key exchange only exists with the MCU: own keys go to the publisher, the remote keys to each subscriber.
private fun keyRingsFor(
isMCUPublisher: Boolean,
hasMCU: Boolean,
sessionId: String?
): Pair<FrameKeyRing?, FrameKeyRing?> {
val callEncryption = callEncryption() ?: return null to null
return when {
isMCUPublisher -> callEncryption.ownKeyRing to null
hasMCU && sessionId != null -> null to callEncryption.keyRing(sessionId)
else -> null to null
}
}

private fun addCallParticipant(sessionId: String?) {
Expand Down
Loading
Loading