mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-21 17:05:39 +03:00
WIP
refact WIP TMP WIP
This commit is contained in:
parent
d2f1488934
commit
dc19652c2b
44 changed files with 3710 additions and 30 deletions
|
@ -162,6 +162,10 @@ dependencies {
|
|||
// Phone number https://github.com/google/libphonenumber
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
|
||||
|
||||
// Web RTC
|
||||
// TODO meant for development purposes only
|
||||
implementation 'org.webrtc:google-webrtc:1.0.+'
|
||||
|
||||
debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0'
|
||||
releaseImplementation 'com.airbnb.okreplay:noop:1.5.0'
|
||||
androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0'
|
||||
|
|
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.api.pushrules.PushRuleService
|
|||
import im.vector.matrix.android.api.session.account.AccountService
|
||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||
import im.vector.matrix.android.api.session.cache.CacheService
|
||||
import im.vector.matrix.android.api.session.call.CallService
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
|
@ -165,6 +166,11 @@ interface Session :
|
|||
*/
|
||||
fun integrationManagerService(): IntegrationManagerService
|
||||
|
||||
/**
|
||||
* Returns the cryptoService associated with the session
|
||||
*/
|
||||
fun callService(): CallService
|
||||
|
||||
/**
|
||||
* Add a listener to the session.
|
||||
* @param listener the listener to add.
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.SessionDescription
|
||||
|
||||
interface CallService {
|
||||
|
||||
|
||||
fun getTurnServer(callback: MatrixCallback<TurnServer?>)
|
||||
|
||||
fun isCallSupportedInRoom(roomId: String) : Boolean
|
||||
|
||||
|
||||
/**
|
||||
* Send offer SDP to the other participant.
|
||||
*/
|
||||
fun sendOfferSdp(callId: String, roomId: String, sdp: SessionDescription, callback: MatrixCallback<String>)
|
||||
|
||||
/**
|
||||
* Send answer SDP to the other participant.
|
||||
*/
|
||||
fun sendAnswerSdp(callId: String, roomId: String, sdp: SessionDescription, callback: MatrixCallback<String>)
|
||||
|
||||
/**
|
||||
* Send Ice candidate to the other participant.
|
||||
*/
|
||||
fun sendLocalIceCandidates(callId: String, roomId: String, candidates: List<IceCandidate>)
|
||||
|
||||
/**
|
||||
* Send removed ICE candidates to the other participant.
|
||||
*/
|
||||
fun sendLocalIceCandidateRemovals(callId: String, roomId: String, candidates: List<IceCandidate>)
|
||||
|
||||
|
||||
fun addCallListener(listener: CallsListener)
|
||||
|
||||
fun removeCallListener(listener: CallsListener)
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallAnswerContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
|
||||
interface CallsListener {
|
||||
// /**
|
||||
// * Called when there is an incoming call within the room.
|
||||
// * @param peerSignalingClient the incoming call
|
||||
// */
|
||||
// fun onIncomingCall(peerSignalingClient: PeerSignalingClient)
|
||||
//
|
||||
// /**
|
||||
// * An outgoing call is started.
|
||||
// *
|
||||
// * @param peerSignalingClient the outgoing call
|
||||
// */
|
||||
// fun onOutgoingCall(peerSignalingClient: PeerSignalingClient)
|
||||
//
|
||||
// /**
|
||||
// * Called when a called has been hung up
|
||||
// *
|
||||
// * @param peerSignalingClient the incoming call
|
||||
// */
|
||||
// fun onCallHangUp(peerSignalingClient: PeerSignalingClient)
|
||||
|
||||
fun onCallInviteReceived(signalingRoomId: String, callInviteContent: CallInviteContent)
|
||||
|
||||
fun onCallAnswerReceived(callAnswerContent: CallAnswerContent)
|
||||
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import org.webrtc.EglBase
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* The root [EglBase] instance shared by the entire application for
|
||||
* the sake of reducing the utilization of system resources (such as EGL
|
||||
* contexts)
|
||||
* by performing a runtime check.
|
||||
*/
|
||||
object EglUtils {
|
||||
|
||||
// TODO how do we release that?
|
||||
|
||||
/**
|
||||
* Lazily creates and returns the one and only [EglBase] which will
|
||||
* serve as the root for all contexts that are needed.
|
||||
*/
|
||||
@get:Synchronized var rootEglBase: EglBase? = null
|
||||
get() {
|
||||
if (field == null) {
|
||||
val configAttributes = EglBase.CONFIG_PLAIN
|
||||
try {
|
||||
field = EglBase.createEgl14(configAttributes)
|
||||
?: EglBase.createEgl10(configAttributes) // Fall back to EglBase10.
|
||||
} catch (ex: Throwable) {
|
||||
Timber.e(ex, "Failed to create EglBase")
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
private set
|
||||
|
||||
val rootEglBaseContext: EglBase.Context?
|
||||
get() {
|
||||
val eglBase = rootEglBase
|
||||
return eglBase?.eglBaseContext
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
///*
|
||||
// * Copyright (c) 2020 New Vector Ltd
|
||||
// *
|
||||
// * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// * you may not use this file except in compliance with the License.
|
||||
// * You may obtain a copy of the License at
|
||||
// *
|
||||
// * http://www.apache.org/licenses/LICENSE-2.0
|
||||
// *
|
||||
// * Unless required by applicable law or agreed to in writing, software
|
||||
// * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// * See the License for the specific language governing permissions and
|
||||
// * limitations under the License.
|
||||
// */
|
||||
//
|
||||
//package im.vector.matrix.android.api.session.call
|
||||
//
|
||||
//import im.vector.matrix.android.api.MatrixCallback
|
||||
//import org.webrtc.IceCandidate
|
||||
//import org.webrtc.SessionDescription
|
||||
//
|
||||
//interface PeerSignalingClient {
|
||||
//
|
||||
// val callID: String
|
||||
//
|
||||
// fun addListener(listener: SignalingListener)
|
||||
//
|
||||
// /**
|
||||
// * Send offer SDP to the other participant.
|
||||
// */
|
||||
// fun sendOfferSdp(sdp: SessionDescription, callback: MatrixCallback<String>)
|
||||
//
|
||||
// /**
|
||||
// * Send answer SDP to the other participant.
|
||||
// */
|
||||
// fun sendAnswerSdp(sdp: SessionDescription, callback: MatrixCallback<String>)
|
||||
//
|
||||
// /**
|
||||
// * Send Ice candidate to the other participant.
|
||||
// */
|
||||
// fun sendLocalIceCandidates(candidates: List<IceCandidate>)
|
||||
//
|
||||
// /**
|
||||
// * Send removed ICE candidates to the other participant.
|
||||
// */
|
||||
// fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>)
|
||||
//
|
||||
//
|
||||
// interface SignalingListener {
|
||||
// /**
|
||||
// * Callback fired once remote SDP is received.
|
||||
// */
|
||||
// fun onRemoteDescription(sdp: SessionDescription)
|
||||
//
|
||||
// /**
|
||||
// * Callback fired once remote Ice candidate is received.
|
||||
// */
|
||||
// fun onRemoteIceCandidate(candidate: IceCandidate)
|
||||
//
|
||||
// /**
|
||||
// * Callback fired once remote Ice candidate removals are received.
|
||||
// */
|
||||
// fun onRemoteIceCandidatesRemoved(candidates: List<IceCandidate>)
|
||||
// }
|
||||
//}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.PeerConnection.IceServer
|
||||
import org.webrtc.SessionDescription
|
||||
|
||||
/**
|
||||
* Struct holding the connection parameters of an AppRTC room.
|
||||
*/
|
||||
data class RoomConnectionParameters(
|
||||
val callId: String,
|
||||
val matrixRoomId: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Struct holding the signaling parameters of an AppRTC room.
|
||||
*/
|
||||
data class SignalingParameters(
|
||||
val iceServers: List<IceServer>,
|
||||
val initiator: Boolean,
|
||||
val clientId: String,
|
||||
val offerSdp: SessionDescription,
|
||||
val iceCandidates: List<IceCandidate>
|
||||
)
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TurnServer(
|
||||
@Json(name = "username") val username: String?,
|
||||
@Json(name = "password") val password: String?,
|
||||
@Json(name = "uris") val uris: List<String>?,
|
||||
@Json(name = "ttl") val ttl: Int?
|
||||
)
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
|
||||
internal interface VoipApi {
|
||||
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "voip/turnServer")
|
||||
fun getTurnServer(): Call<TurnServer>
|
||||
|
||||
}
|
|
@ -58,7 +58,6 @@ object EventType {
|
|||
const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
|
||||
|
||||
// Call Events
|
||||
|
||||
const val CALL_INVITE = "m.call.invite"
|
||||
const val CALL_CANDIDATES = "m.call.candidates"
|
||||
const val CALL_ANSWER = "m.call.answer"
|
||||
|
|
|
@ -21,21 +21,42 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CallInviteContent(
|
||||
@Json(name = "call_id") val callId: String,
|
||||
@Json(name = "version") val version: Int,
|
||||
@Json(name = "lifetime") val lifetime: Int,
|
||||
@Json(name = "offer") val offer: Offer
|
||||
|
||||
/**
|
||||
* A unique identifier for the call.
|
||||
*/
|
||||
@Json(name = "call_id") val callId: String?,
|
||||
/**
|
||||
* The session description object
|
||||
*/
|
||||
@Json(name = "version") val version: Int?,
|
||||
/**
|
||||
* The version of the VoIP specification this message adheres to. This specification is version 0.
|
||||
*/
|
||||
@Json(name = "lifetime") val lifetime: Int?,
|
||||
/**
|
||||
* The time in milliseconds that the invite is valid for.
|
||||
* Once the invite age exceeds this value, clients should discard it.
|
||||
* They should also no longer show the call as awaiting an answer in the UI.
|
||||
*/
|
||||
@Json(name = "offer") val offer: Offer?
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Offer(
|
||||
@Json(name = "type") val type: String,
|
||||
@Json(name = "sdp") val sdp: String
|
||||
/**
|
||||
* The type of session description (offer, answer)
|
||||
*/
|
||||
@Json(name = "type") val type: String?,
|
||||
/**
|
||||
* The SDP text of the session description.
|
||||
*/
|
||||
@Json(name = "sdp") val sdp: String?
|
||||
) {
|
||||
companion object {
|
||||
const val SDP_VIDEO = "m=video"
|
||||
}
|
||||
}
|
||||
|
||||
fun isVideo(): Boolean = offer.sdp.contains(Offer.SDP_VIDEO)
|
||||
fun isVideo(): Boolean = offer?.sdp?.contains(Offer.SDP_VIDEO) == true
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ import im.vector.matrix.android.internal.crypto.tasks.DefaultEncryptEventTask
|
|||
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDeviceInfoTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDevicesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultInitializeCrossSigningTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendEventTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendToDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSetDeviceNameTask
|
||||
|
@ -80,6 +81,7 @@ import im.vector.matrix.android.internal.crypto.tasks.EncryptEventTask
|
|||
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.InitializeCrossSigningTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendEventTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
||||
|
@ -251,4 +253,7 @@ internal abstract class CryptoModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.crypto.tasks
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater
|
||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface SendEventTask : Task<SendEventTask.Params, String> {
|
||||
data class Params(
|
||||
val event: Event,
|
||||
val cryptoService: CryptoService?
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultSendEventTask @Inject constructor(
|
||||
private val localEchoUpdater: LocalEchoUpdater,
|
||||
private val encryptEventTask: DefaultEncryptEventTask,
|
||||
private val roomAPI: RoomAPI,
|
||||
private val eventBus: EventBus) : SendEventTask {
|
||||
|
||||
override suspend fun execute(params: SendEventTask.Params): String {
|
||||
val event = handleEncryption(params)
|
||||
val localId = event.eventId!!
|
||||
|
||||
try {
|
||||
localEchoUpdater.updateSendState(localId, SendState.SENDING)
|
||||
val executeRequest = executeRequest<SendResponse>(eventBus) {
|
||||
apiCall = roomAPI.send(
|
||||
localId,
|
||||
roomId = event.roomId ?: "",
|
||||
content = event.content,
|
||||
eventType = event.type
|
||||
)
|
||||
}
|
||||
localEchoUpdater.updateSendState(localId, SendState.SENT)
|
||||
return executeRequest.eventId
|
||||
} catch (e: Throwable) {
|
||||
localEchoUpdater.updateSendState(localId, SendState.UNDELIVERED)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleEncryption(params: SendEventTask.Params): Event {
|
||||
if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) {
|
||||
try {
|
||||
return encryptEventTask.execute(EncryptEventTask.Params(
|
||||
params.event.roomId ?: "",
|
||||
params.event,
|
||||
listOf("m.relates_to"),
|
||||
params.cryptoService
|
||||
))
|
||||
} catch (throwable: Throwable) {
|
||||
// We said it's ok to send verification request in clear
|
||||
}
|
||||
}
|
||||
return params.event
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.Session
|
|||
import im.vector.matrix.android.api.session.account.AccountService
|
||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||
import im.vector.matrix.android.api.session.cache.CacheService
|
||||
import im.vector.matrix.android.api.session.call.CallService
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
|
@ -110,7 +111,8 @@ internal class DefaultSession @Inject constructor(
|
|||
private val integrationManagerService: IntegrationManagerService,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val widgetDependenciesHolder: WidgetDependenciesHolder,
|
||||
private val shieldTrustUpdater: ShieldTrustUpdater)
|
||||
private val shieldTrustUpdater: ShieldTrustUpdater,
|
||||
private val callService: Lazy<CallService>)
|
||||
: Session,
|
||||
RoomService by roomService.get(),
|
||||
RoomDirectoryService by roomDirectoryService.get(),
|
||||
|
@ -245,6 +247,8 @@ internal class DefaultSession @Inject constructor(
|
|||
|
||||
override fun integrationManagerService() = integrationManagerService
|
||||
|
||||
override fun callService(): CallService = callService.get()
|
||||
|
||||
override fun addListener(listener: Session.Listener) {
|
||||
sessionListeners.addListener(listener)
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import im.vector.matrix.android.api.crypto.MXCryptoConfig
|
|||
import im.vector.matrix.android.api.session.InitialSyncProgressService
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||
import im.vector.matrix.android.api.session.call.CallService
|
||||
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
|
||||
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
|
||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
||||
|
@ -56,9 +57,9 @@ import im.vector.matrix.android.internal.network.NetworkCallbackStrategy
|
|||
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
||||
import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy
|
||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||
import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor
|
||||
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
||||
import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider
|
||||
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
|
||||
import im.vector.matrix.android.internal.session.call.CallEventObserver
|
||||
import im.vector.matrix.android.internal.session.call.DefaultCallService
|
||||
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService
|
||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||
|
@ -245,7 +246,11 @@ internal abstract class SessionModule {
|
|||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindRoomTombstoneEventLiveObserver(observer: RoomTombstoneEventLiveObserver): LiveEntityObserver
|
||||
abstract fun bindCallEventObserver(callEventObserver: CallEventObserver): LiveEntityObserver
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindRoomTombstoneEventLiveObserver(roomTombstoneEventLiveObserver: RoomTombstoneEventLiveObserver): LiveEntityObserver
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
|
@ -269,4 +274,7 @@ internal abstract class SessionModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindSharedSecretStorageService(service: DefaultSharedSecretStorageService): SharedSecretStorageService
|
||||
|
||||
@Binds
|
||||
abstract fun bindCallService(service:DefaultCallService): CallService
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.call
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.whereTypes
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationTask
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class CallEventObserver @Inject constructor(
|
||||
@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
@UserId private val userId: String,
|
||||
private val task: CallEventsObserverTask) :
|
||||
RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||
|
||||
override val query = Monarchy.Query<EventEntity> {
|
||||
EventEntity.whereTypes(it, listOf(
|
||||
EventType.CALL_ANSWER,
|
||||
EventType.CALL_CANDIDATES,
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.ENCRYPTED)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||
Timber.v("EventRelationsAggregationUpdater called with ${changeSet.insertions.size} insertions")
|
||||
|
||||
val insertedDomains = changeSet.insertions
|
||||
.asSequence()
|
||||
.mapNotNull { results[it]?.asDomain() }
|
||||
.toList()
|
||||
|
||||
val params = CallEventsObserverTask.Params(
|
||||
insertedDomains,
|
||||
userId
|
||||
)
|
||||
observerScope.launch {
|
||||
task.execute(params)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.call
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||
import io.realm.Realm
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface CallEventsObserverTask : Task<CallEventsObserverTask.Params, Unit> {
|
||||
|
||||
data class Params(
|
||||
val events: List<Event>,
|
||||
val userId: String
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultCallEventsObserverTask @Inject constructor(
|
||||
private val monarchy: Monarchy,
|
||||
private val cryptoService: CryptoService,
|
||||
private val callService: DefaultCallService) : CallEventsObserverTask {
|
||||
|
||||
override suspend fun execute(params: CallEventsObserverTask.Params) {
|
||||
val events = params.events
|
||||
val userId = params.userId
|
||||
monarchy.awaitTransaction { realm ->
|
||||
Timber.v(">>> DefaultCallEventsObserverTask[${params.hashCode()}] called with ${events.size} events")
|
||||
update(realm, events, userId)
|
||||
Timber.v("<<< DefaultCallEventsObserverTask[${params.hashCode()}] finished")
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(realm: Realm, events: List<Event>, userId: String) {
|
||||
events.forEach { event ->
|
||||
event.roomId ?: return@forEach Unit.also {
|
||||
Timber.w("Event with no room id ${event.eventId}")
|
||||
}
|
||||
decryptIfNeeded(event)
|
||||
when (event.getClearType()) {
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_CANDIDATES,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> {
|
||||
callService.onCallEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
Timber.v("$realm : $userId")
|
||||
}
|
||||
|
||||
private fun decryptIfNeeded(event: Event) {
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, event.roomId ?: "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.v("Call service: Failed to decrypt event")
|
||||
// TODO -> we should keep track of this and retry, or aggregation will be broken
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.call
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
import im.vector.matrix.android.api.session.call.CallService
|
||||
import im.vector.matrix.android.api.session.call.CallsListener
|
||||
import im.vector.matrix.android.api.session.call.TurnServer
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.UnsignedData
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallAnswerContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallCandidatesContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
||||
import im.vector.matrix.android.internal.session.room.send.RoomEventSender
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.SessionDescription
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class DefaultCallService @Inject constructor(
|
||||
@UserId
|
||||
private val userId: String,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val roomEventSender: RoomEventSender
|
||||
) : CallService {
|
||||
|
||||
private val callListeners = ArrayList<CallsListener>()
|
||||
|
||||
override fun getTurnServer(callback: MatrixCallback<TurnServer?>) {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun isCallSupportedInRoom(roomId: String): Boolean {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun sendOfferSdp(callId: String, roomId: String, sdp: SessionDescription, callback: MatrixCallback<String>) {
|
||||
val eventContent = CallInviteContent(
|
||||
callId = callId,
|
||||
version = 0,
|
||||
lifetime = CALL_TIMEOUT_MS,
|
||||
offer = CallInviteContent.Offer(
|
||||
type = sdp.type.canonicalForm(),
|
||||
sdp = sdp.description
|
||||
)
|
||||
)
|
||||
|
||||
createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = eventContent.toContent()).let { event ->
|
||||
roomEventSender.sendEvent(event)
|
||||
// sendEventTask
|
||||
// .configureWith(
|
||||
// SendEventTask.Params(event = event, cryptoService = cryptoService)
|
||||
// ) {
|
||||
// this.callback = callback
|
||||
// }.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendAnswerSdp(callId: String, roomId: String, sdp: SessionDescription, callback: MatrixCallback<String>) {
|
||||
val eventContent = CallAnswerContent(
|
||||
callId = callId,
|
||||
version = 0,
|
||||
answer = CallAnswerContent.Answer(
|
||||
type = sdp.type.canonicalForm(),
|
||||
sdp = sdp.description
|
||||
)
|
||||
)
|
||||
|
||||
createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = eventContent.toContent()).let { event ->
|
||||
roomEventSender.sendEvent(event)
|
||||
// sendEventTask
|
||||
// .configureWith(
|
||||
// SendEventTask.Params(event = event, cryptoService = cryptoService)
|
||||
// ) {
|
||||
// this.callback = callback
|
||||
// }.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendLocalIceCandidates(callId: String, roomId: String, candidates: List<IceCandidate>) {
|
||||
val eventContent = CallCandidatesContent(
|
||||
callId = callId,
|
||||
version = 0,
|
||||
candidates = candidates.map {
|
||||
CallCandidatesContent.Candidate(
|
||||
sdpMid = it.sdpMid,
|
||||
sdpMLineIndex = it.sdpMLineIndex.toString(),
|
||||
candidate = it.sdp
|
||||
)
|
||||
}
|
||||
)
|
||||
createEventAndLocalEcho(type = EventType.CALL_CANDIDATES, roomId = roomId, content = eventContent.toContent()).let { event ->
|
||||
roomEventSender.sendEvent(event)
|
||||
// sendEventTask
|
||||
// .configureWith(
|
||||
// SendEventTask.Params(event = event, cryptoService = cryptoService)
|
||||
// ) {
|
||||
// this.callback = callback
|
||||
// }.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendLocalIceCandidateRemovals(callId: String, roomId: String, candidates: List<IceCandidate>) {
|
||||
}
|
||||
|
||||
override fun addCallListener(listener: CallsListener) {
|
||||
if (!callListeners.contains(listener)) callListeners.add(listener)
|
||||
}
|
||||
|
||||
override fun removeCallListener(listener: CallsListener) {
|
||||
callListeners.remove(listener)
|
||||
}
|
||||
|
||||
fun onCallEvent(event: Event) {
|
||||
when (event.getClearType()) {
|
||||
EventType.CALL_ANSWER -> {
|
||||
event.getClearContent().toModel<CallAnswerContent>()?.let {
|
||||
onCallAnswer(it)
|
||||
}
|
||||
}
|
||||
EventType.CALL_INVITE -> {
|
||||
event.getClearContent().toModel<CallInviteContent>()?.let {
|
||||
onCallInvite(event.roomId ?: "", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCallAnswer(answer: CallAnswerContent) {
|
||||
callListeners.forEach {
|
||||
tryThis {
|
||||
it.onCallAnswerReceived(answer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCallInvite(roomId: String, answer: CallInviteContent) {
|
||||
callListeners.forEach {
|
||||
tryThis {
|
||||
it.onCallInviteReceived(roomId, answer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event {
|
||||
return Event(
|
||||
roomId = roomId,
|
||||
originServerTs = System.currentTimeMillis(),
|
||||
senderId = userId,
|
||||
eventId = localId,
|
||||
type = type,
|
||||
content = content,
|
||||
unsignedData = UnsignedData(age = null, transactionId = localId)
|
||||
).also {
|
||||
localEchoEventFactory.createLocalEcho(it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CALL_TIMEOUT_MS = 120_000
|
||||
}
|
||||
|
||||
// internal class PeerSignalingClientFactory @Inject constructor(
|
||||
// @UserId private val userId: String,
|
||||
// private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
// private val sendEventTask: SendEventTask,
|
||||
// private val taskExecutor: TaskExecutor,
|
||||
// private val cryptoService: CryptoService
|
||||
// ) {
|
||||
//
|
||||
// fun create(roomId: String, callId: String): PeerSignalingClient {
|
||||
// return RoomPeerSignalingClient(
|
||||
// callID = callId,
|
||||
// roomId = roomId,
|
||||
// userId = userId,
|
||||
// localEchoEventFactory = localEchoEventFactory,
|
||||
// sendEventTask = sendEventTask,
|
||||
// taskExecutor = taskExecutor,
|
||||
// cryptoService = cryptoService
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -24,6 +24,8 @@ import im.vector.matrix.android.api.session.room.RoomDirectoryService
|
|||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import im.vector.matrix.android.internal.session.DefaultFileService
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.call.CallEventsObserverTask
|
||||
import im.vector.matrix.android.internal.session.call.DefaultCallEventsObserverTask
|
||||
import im.vector.matrix.android.internal.session.room.alias.DefaultGetRoomIdByAliasTask
|
||||
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
|
||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
||||
|
@ -201,4 +203,8 @@ internal abstract class RoomModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindDeleteTagFromRoomTask(task: DefaultDeleteTagFromRoomTask): DeleteTagFromRoomTask
|
||||
|
||||
// TODO is this in the correct module?
|
||||
@Binds
|
||||
abstract fun bindCallEventsObserverTask(task: DefaultCallEventsObserverTask): CallEventsObserverTask
|
||||
}
|
||||
|
|
|
@ -58,7 +58,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val cryptoService: CryptoService,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val localEchoRepository: LocalEchoRepository
|
||||
private val localEchoRepository: LocalEchoRepository,
|
||||
private val roomEventSender: RoomEventSender
|
||||
) : SendService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
|
@ -111,20 +112,6 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
.let { sendEvent(it) }
|
||||
}
|
||||
|
||||
private fun sendEvent(event: Event): Cancelable {
|
||||
// Encrypted room handling
|
||||
return if (cryptoService.isRoomEncrypted(roomId)) {
|
||||
Timber.v("Send event in encrypted room")
|
||||
val encryptWork = createEncryptEventWork(event, true)
|
||||
// Note that event will be replaced by the result of the previous work
|
||||
val sendWork = createSendEventWork(event, false)
|
||||
timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork)
|
||||
} else {
|
||||
val sendWork = createSendEventWork(event, true)
|
||||
timelineSendEventWorkCommon.postWork(roomId, sendWork)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendMedias(attachments: List<ContentAttachmentData>,
|
||||
compressBeforeSending: Boolean,
|
||||
roomIds: Set<String>): Cancelable {
|
||||
|
@ -269,6 +256,10 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
return cancelableBag
|
||||
}
|
||||
|
||||
private fun sendEvent(event: Event): Cancelable {
|
||||
return roomEventSender.sendEvent(event)
|
||||
}
|
||||
|
||||
private fun createLocalEcho(event: Event) {
|
||||
localEchoEventFactory.createLocalEcho(event)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.room.send
|
||||
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.di.SessionId
|
||||
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import im.vector.matrix.android.internal.worker.startChain
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomEventSender @Inject constructor(
|
||||
private val workManagerProvider: WorkManagerProvider,
|
||||
private val timelineSendEventWorkCommon: TimelineSendEventWorkCommon,
|
||||
@SessionId private val sessionId: String,
|
||||
private val cryptoService: CryptoService
|
||||
) {
|
||||
fun sendEvent(event: Event): Cancelable {
|
||||
// Encrypted room handling
|
||||
return if (cryptoService.isRoomEncrypted(event.roomId ?: "")) {
|
||||
Timber.v("Send event in encrypted room")
|
||||
val encryptWork = createEncryptEventWork(event, true)
|
||||
// Note that event will be replaced by the result of the previous work
|
||||
val sendWork = createSendEventWork(event, false)
|
||||
timelineSendEventWorkCommon.postSequentialWorks(event.roomId ?: "", encryptWork, sendWork)
|
||||
} else {
|
||||
val sendWork = createSendEventWork(event, true)
|
||||
timelineSendEventWorkCommon.postWork(event.roomId ?: "", sendWork)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
// Same parameter
|
||||
val params = EncryptEventWorker.Params(sessionId, event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(params)
|
||||
|
||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.setInputData(sendWorkData)
|
||||
.startChain(startChain)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId, event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
||||
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||
}
|
||||
}
|
|
@ -390,6 +390,9 @@ dependencies {
|
|||
|
||||
implementation 'com.github.BillCarsonFr:JsonViewer:0.5'
|
||||
|
||||
// TODO meant for development purposes only
|
||||
implementation 'org.webrtc:google-webrtc:1.0.+'
|
||||
|
||||
// QR-code
|
||||
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
|
|
|
@ -35,6 +35,7 @@ import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
|
|||
import im.vector.riotx.core.utils.allGranted
|
||||
import im.vector.riotx.core.utils.checkPermissions
|
||||
import im.vector.riotx.core.utils.toast
|
||||
import im.vector.riotx.features.call.VectorCallActivity
|
||||
import im.vector.riotx.features.debug.sas.DebugSasEmojiActivity
|
||||
import im.vector.riotx.features.qrcode.QrCodeScannerActivity
|
||||
import kotlinx.android.synthetic.debug.activity_debug_menu.*
|
||||
|
@ -183,7 +184,8 @@ class DebugMenuActivity : VectorBaseActivity() {
|
|||
@OnClick(R.id.debug_scan_qr_code)
|
||||
fun scanQRCode() {
|
||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
|
||||
doScanQRCode()
|
||||
//doScanQRCode()
|
||||
startActivity(VectorCallActivity.newIntent(this, "!cyIJhOLwWgmmqreHLD:matrix.org"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,8 +8,16 @@
|
|||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- Call feature -->
|
||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
|
||||
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
|
||||
<!-- READ_PHONE_STATE is needed only if your calling app reads numbers from the `PHONE_STATE`
|
||||
intent action. -->
|
||||
|
||||
|
||||
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
|
||||
<!-- Tell that the Camera is not mandatory to install the application -->
|
||||
<uses-feature
|
||||
|
@ -172,6 +180,7 @@
|
|||
<activity
|
||||
android:name=".features.attachments.preview.AttachmentsPreviewActivity"
|
||||
android:theme="@style/AppTheme.AttachmentsPreview" />
|
||||
<activity android:name=".features.call.VectorCallActivity" />
|
||||
|
||||
<activity android:name=".features.terms.ReviewTermsActivity" />
|
||||
<activity android:name=".features.widgets.WidgetActivity" />
|
||||
|
@ -186,6 +195,13 @@
|
|||
android:name=".core.services.VectorSyncService"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name=".features.call.VectorConnectionService"
|
||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.ConnectionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- Receivers -->
|
||||
|
||||
<!-- Exported false, should only be accessible from this app!! -->
|
||||
|
|
|
@ -43,6 +43,7 @@ import im.vector.riotx.core.di.HasVectorInjector
|
|||
import im.vector.riotx.core.di.VectorComponent
|
||||
import im.vector.riotx.core.extensions.configureAndStart
|
||||
import im.vector.riotx.core.rx.RxConfig
|
||||
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.riotx.features.configuration.VectorConfiguration
|
||||
import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
|
||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||
|
@ -80,6 +81,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
|||
@Inject lateinit var appStateHandler: AppStateHandler
|
||||
@Inject lateinit var rxConfig: RxConfig
|
||||
@Inject lateinit var popupAlertManager: PopupAlertManager
|
||||
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||
|
||||
lateinit var vectorComponent: VectorComponent
|
||||
private var fontThreadHandler: Handler? = null
|
||||
|
||||
|
@ -122,6 +125,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
|||
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
|
||||
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
|
||||
lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
|
||||
lastAuthenticatedSession.callService().addCallListener(webRtcPeerConnectionManager)
|
||||
}
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
|
|
|
@ -24,6 +24,7 @@ import dagger.Component
|
|||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.core.preference.UserAvatarPreference
|
||||
import im.vector.riotx.features.MainActivity
|
||||
import im.vector.riotx.features.call.VectorCallActivity
|
||||
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
|
||||
|
@ -130,6 +131,7 @@ interface ScreenComponent {
|
|||
fun inject(activity: InviteUsersToRoomActivity)
|
||||
fun inject(activity: ReviewTermsActivity)
|
||||
fun inject(activity: WidgetActivity)
|
||||
fun inject(activity: VectorCallActivity)
|
||||
|
||||
/* ==========================================================================================
|
||||
* BottomSheets
|
||||
|
|
|
@ -31,6 +31,7 @@ import im.vector.riotx.core.error.ErrorFormatter
|
|||
import im.vector.riotx.core.pushers.PushersManager
|
||||
import im.vector.riotx.core.utils.AssetReader
|
||||
import im.vector.riotx.core.utils.DimensionConverter
|
||||
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.riotx.features.configuration.VectorConfiguration
|
||||
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
|
||||
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
|
||||
|
@ -134,6 +135,8 @@ interface VectorComponent {
|
|||
|
||||
fun reAuthHelper(): ReAuthHelper
|
||||
|
||||
fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(@BindsInstance context: Context): VectorComponent
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.call
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.telecom.Connection
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M) class CallConnection(
|
||||
private val context: Context,
|
||||
private val roomId: String,
|
||||
private val callId: String
|
||||
) : Connection() {
|
||||
|
||||
/**
|
||||
* The telecom subsystem calls this method when you add a new incoming call and your app should show its incoming call UI.
|
||||
*/
|
||||
override fun onShowIncomingCallUi() {
|
||||
VectorCallActivity.newIntent(context, roomId).let {
|
||||
context.startActivity(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnswer() {
|
||||
super.onAnswer()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
///*
|
||||
// * Copyright (c) 2020 New Vector Ltd
|
||||
// *
|
||||
// * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// * you may not use this file except in compliance with the License.
|
||||
// * You may obtain a copy of the License at
|
||||
// *
|
||||
// * http://www.apache.org/licenses/LICENSE-2.0
|
||||
// *
|
||||
// * Unless required by applicable law or agreed to in writing, software
|
||||
// * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// * See the License for the specific language governing permissions and
|
||||
// * limitations under the License.
|
||||
// */
|
||||
//
|
||||
//package im.vector.riotx.features.call;
|
||||
//
|
||||
//import android.os.Bundle;
|
||||
//import android.view.LayoutInflater;
|
||||
//import android.view.View;
|
||||
//import android.view.ViewGroup;
|
||||
//import android.widget.ImageButton;
|
||||
//import android.widget.SeekBar;
|
||||
//import android.widget.TextView;
|
||||
//import org.webrtc.RendererCommon.ScalingType;
|
||||
//
|
||||
//import androidx.fragment.app.Fragment;
|
||||
//
|
||||
//import im.vector.riotx.R;
|
||||
//
|
||||
///**
|
||||
// * Fragment for call control.
|
||||
// */
|
||||
//public class CallFragment extends Fragment {
|
||||
// private TextView contactView;
|
||||
// private ImageButton cameraSwitchButton;
|
||||
// private ImageButton videoScalingButton;
|
||||
// private ImageButton toggleMuteButton;
|
||||
// private TextView captureFormatText;
|
||||
// private SeekBar captureFormatSlider;
|
||||
// private OnCallEvents callEvents;
|
||||
// private ScalingType scalingType;
|
||||
// private boolean videoCallEnabled = true;
|
||||
// /**
|
||||
// * Call control interface for container activity.
|
||||
// */
|
||||
// public interface OnCallEvents {
|
||||
// void onCallHangUp();
|
||||
// void onCameraSwitch();
|
||||
// void onVideoScalingSwitch(ScalingType scalingType);
|
||||
// void onCaptureFormatChange(int width, int height, int framerate);
|
||||
// boolean onToggleMic();
|
||||
// }
|
||||
// @Override
|
||||
// public View onCreateView(
|
||||
// LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
// View controlView = inflater.inflate(R.layout.fragment_call, container, false);
|
||||
// // Create UI controls.
|
||||
// contactView = controlView.findViewById(R.id.contact_name_call);
|
||||
// ImageButton disconnectButton = controlView.findViewById(R.id.button_call_disconnect);
|
||||
// cameraSwitchButton = controlView.findViewById(R.id.button_call_switch_camera);
|
||||
// videoScalingButton = controlView.findViewById(R.id.button_call_scaling_mode);
|
||||
// toggleMuteButton = controlView.findViewById(R.id.button_call_toggle_mic);
|
||||
// captureFormatText = controlView.findViewById(R.id.capture_format_text_call);
|
||||
// captureFormatSlider = controlView.findViewById(R.id.capture_format_slider_call);
|
||||
// // Add buttons click events.
|
||||
// disconnectButton.setOnClickListener(new View.OnClickListener() {
|
||||
// @Override
|
||||
// public void onClick(View view) {
|
||||
// callEvents.onCallHangUp();
|
||||
// }
|
||||
// });
|
||||
// cameraSwitchButton.setOnClickListener(new View.OnClickListener() {
|
||||
// @Override
|
||||
// public void onClick(View view) {
|
||||
// callEvents.onCameraSwitch();
|
||||
// }
|
||||
// });
|
||||
// videoScalingButton.setOnClickListener(new View.OnClickListener() {
|
||||
// @Override
|
||||
// public void onClick(View view) {
|
||||
// if (scalingType == ScalingType.SCALE_ASPECT_FILL) {
|
||||
// videoScalingButton.setBackgroundResource(R.drawable.ic_action_full_screen);
|
||||
// scalingType = ScalingType.SCALE_ASPECT_FIT;
|
||||
// } else {
|
||||
// videoScalingButton.setBackgroundResource(R.drawable.ic_action_return_from_full_screen);
|
||||
// scalingType = ScalingType.SCALE_ASPECT_FILL;
|
||||
// }
|
||||
// callEvents.onVideoScalingSwitch(scalingType);
|
||||
// }
|
||||
// });
|
||||
// scalingType = ScalingType.SCALE_ASPECT_FILL;
|
||||
// toggleMuteButton.setOnClickListener(new View.OnClickListener() {
|
||||
// @Override
|
||||
// public void onClick(View view) {
|
||||
// boolean enabled = callEvents.onToggleMic();
|
||||
// toggleMuteButton.setAlpha(enabled ? 1.0f : 0.3f);
|
||||
// }
|
||||
// });
|
||||
// return controlView;
|
||||
// }
|
||||
// @Override
|
||||
// public void onStart() {
|
||||
// super.onStart();
|
||||
// boolean captureSliderEnabled = false;
|
||||
// Bundle args = getArguments();
|
||||
// if (args != null) {
|
||||
// String contactName = args.getString(CallActivity.EXTRA_ROOMID);
|
||||
// contactView.setText(contactName);
|
||||
// videoCallEnabled = args.getBoolean(CallActivity.EXTRA_VIDEO_CALL, true);
|
||||
// captureSliderEnabled = videoCallEnabled
|
||||
// && args.getBoolean(CallActivity.EXTRA_VIDEO_CAPTUREQUALITYSLIDER_ENABLED, false);
|
||||
// }
|
||||
// if (!videoCallEnabled) {
|
||||
// cameraSwitchButton.setVisibility(View.INVISIBLE);
|
||||
// }
|
||||
// if (captureSliderEnabled) {
|
||||
// captureFormatSlider.setOnSeekBarChangeListener(
|
||||
// new CaptureQualityController(captureFormatText, callEvents));
|
||||
// } else {
|
||||
// captureFormatText.setVisibility(View.GONE);
|
||||
// captureFormatSlider.setVisibility(View.GONE);
|
||||
// }
|
||||
// }
|
||||
// // TODO(sakal): Replace with onAttach(Context) once we only support API level 23+.
|
||||
// @SuppressWarnings("deprecation")
|
||||
// @Override
|
||||
// public void onAttach(Activity activity) {
|
||||
// super.onAttach(activity);
|
||||
// callEvents = (OnCallEvents) activity;
|
||||
// }
|
||||
//}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.call
|
||||
|
||||
import org.webrtc.DataChannel
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.MediaStream
|
||||
import org.webrtc.PeerConnection
|
||||
import org.webrtc.RtpReceiver
|
||||
import timber.log.Timber
|
||||
|
||||
abstract class PeerConnectionObserverAdapter : PeerConnection.Observer {
|
||||
override fun onIceCandidate(p0: IceCandidate?) {
|
||||
Timber.v("## VOIP onIceCandidate $p0")
|
||||
}
|
||||
|
||||
override fun onDataChannel(p0: DataChannel?) {
|
||||
Timber.v("## VOIP onDataChannel $p0")
|
||||
}
|
||||
|
||||
override fun onIceConnectionReceivingChange(p0: Boolean) {
|
||||
Timber.v("## VOIP onIceConnectionReceivingChange $p0")
|
||||
}
|
||||
|
||||
override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) {
|
||||
Timber.v("## VOIP onIceConnectionChange $p0")
|
||||
}
|
||||
|
||||
override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) {
|
||||
Timber.v("## VOIP onIceConnectionChange $p0")
|
||||
}
|
||||
|
||||
override fun onAddStream(mediaStream: MediaStream?) {
|
||||
Timber.v("## VOIP onAddStream $mediaStream")
|
||||
}
|
||||
|
||||
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
|
||||
Timber.v("## VOIP onSignalingChange $p0")
|
||||
}
|
||||
|
||||
override fun onIceCandidatesRemoved(p0: Array<out IceCandidate>?) {
|
||||
Timber.v("## VOIP onIceCandidatesRemoved $p0")
|
||||
}
|
||||
|
||||
override fun onRemoveStream(mediaStream: MediaStream?) {
|
||||
Timber.v("## VOIP onRemoveStream $mediaStream")
|
||||
}
|
||||
|
||||
override fun onRenegotiationNeeded() {
|
||||
Timber.v("## VOIP onRenegotiationNeeded")
|
||||
}
|
||||
|
||||
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
|
||||
Timber.v("## VOIP onAddTrack $p0 / out: $p1")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.call
|
||||
|
||||
import org.webrtc.SdpObserver
|
||||
import org.webrtc.SessionDescription
|
||||
import timber.log.Timber
|
||||
|
||||
abstract class SdpObserverAdapter : SdpObserver {
|
||||
override fun onSetFailure(p0: String?) {
|
||||
Timber.e("## SdpObserver: onSetFailure $p0")
|
||||
}
|
||||
|
||||
override fun onSetSuccess() {
|
||||
Timber.v("## SdpObserver: onSetSuccess")
|
||||
}
|
||||
|
||||
override fun onCreateSuccess(p0: SessionDescription?) {
|
||||
Timber.e("## SdpObserver: onSetFailure $p0")
|
||||
}
|
||||
|
||||
override fun onCreateFailure(p0: String?) {
|
||||
Timber.e("## SdpObserver: onSetFailure $p0")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,434 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.call
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import butterknife.BindView
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.viewModel
|
||||
import im.vector.matrix.android.api.session.call.EglUtils
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
|
||||
import im.vector.riotx.core.utils.allGranted
|
||||
import im.vector.riotx.core.utils.checkPermissions
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.webrtc.Camera1Enumerator
|
||||
import org.webrtc.Camera2Enumerator
|
||||
import org.webrtc.EglBase
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.MediaStream
|
||||
import org.webrtc.PeerConnection
|
||||
import org.webrtc.RendererCommon
|
||||
import org.webrtc.SessionDescription
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
import org.webrtc.VideoTrack
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
data class CallArgs(
|
||||
// val callId: String? = null,
|
||||
val roomId: String
|
||||
) : Parcelable
|
||||
|
||||
class VectorCallActivity : VectorBaseActivity(), WebRtcPeerConnectionManager.Listener {
|
||||
|
||||
override fun getLayoutRes() = R.layout.activity_call
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
super.injectWith(injector)
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
private val callViewModel: VectorCallViewModel by viewModel()
|
||||
|
||||
@Inject lateinit var peerConnectionManager: WebRtcPeerConnectionManager
|
||||
|
||||
@Inject lateinit var viewModelFactory: VectorCallViewModel.Factory
|
||||
|
||||
@BindView(R.id.pip_video_view)
|
||||
lateinit var pipRenderer: SurfaceViewRenderer
|
||||
|
||||
@BindView(R.id.fullscreen_video_view)
|
||||
lateinit var fullscreenRenderer: SurfaceViewRenderer
|
||||
|
||||
private var rootEglBase: EglBase? = null
|
||||
|
||||
// private var peerConnectionFactory: PeerConnectionFactory? = null
|
||||
|
||||
//private var peerConnection: PeerConnection? = null
|
||||
|
||||
// private var remoteVideoTrack: VideoTrack? = null
|
||||
|
||||
private val iceCandidateSource: PublishSubject<IceCandidate> = PublishSubject.create()
|
||||
|
||||
override fun doBeforeSetContentView() {
|
||||
// Set window styles for fullscreen-window size. Needs to be done before adding content.
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
setTurnScreenOn(true)
|
||||
setShowWhenLocked(true)
|
||||
getSystemService(KeyguardManager::class.java)?.requestDismissKeyguard(this, null)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
window.addFlags(
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
||||
or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||
or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
||||
)
|
||||
}
|
||||
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
setContentView(R.layout.activity_call)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
rootEglBase = EglUtils.rootEglBase ?: return Unit.also {
|
||||
finish()
|
||||
}
|
||||
|
||||
callViewModel.viewEvents
|
||||
.observe()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
handleViewEvents(it)
|
||||
}
|
||||
.disposeOnDestroy()
|
||||
//
|
||||
// if (isFirstCreation()) {
|
||||
//
|
||||
// }
|
||||
|
||||
if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, this, CAPTURE_PERMISSION_REQUEST_CODE, R.string.permissions_rationale_msg_camera_and_audio)) {
|
||||
start()
|
||||
}
|
||||
peerConnectionManager.listener = this
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
if (requestCode == CAPTURE_PERMISSION_REQUEST_CODE && allGranted(grantResults)) {
|
||||
start()
|
||||
} else {
|
||||
// TODO display something
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun start(): Boolean {
|
||||
// Init Picture in Picture renderer
|
||||
pipRenderer.init(rootEglBase!!.eglBaseContext, null)
|
||||
pipRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
||||
|
||||
// Init Full Screen renderer
|
||||
fullscreenRenderer.init(rootEglBase!!.eglBaseContext, null)
|
||||
fullscreenRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
||||
|
||||
|
||||
pipRenderer.setZOrderMediaOverlay(true);
|
||||
pipRenderer.setEnableHardwareScaler(true /* enabled */);
|
||||
fullscreenRenderer.setEnableHardwareScaler(true /* enabled */);
|
||||
// Start with local feed in fullscreen and swap it to the pip when the call is connected.
|
||||
//setSwappedFeeds(true /* isSwappedFeeds */);
|
||||
|
||||
if (isFirstCreation()) {
|
||||
peerConnectionManager.createPeerConnectionFactory()
|
||||
|
||||
val cameraIterator = if (Camera2Enumerator.isSupported(this)) Camera2Enumerator(this) else Camera1Enumerator(false)
|
||||
val frontCamera = cameraIterator.deviceNames
|
||||
?.firstOrNull { cameraIterator.isFrontFacing(it) }
|
||||
?: cameraIterator.deviceNames?.first()
|
||||
?: return true
|
||||
val videoCapturer = cameraIterator.createCapturer(frontCamera, null)
|
||||
|
||||
val iceServers = ArrayList<PeerConnection.IceServer>().apply {
|
||||
listOf("turn:turn.matrix.org:3478?transport=udp", "turn:turn.matrix.org:3478?transport=tcp", "turns:turn.matrix.org:443?transport=tcp").forEach {
|
||||
add(
|
||||
PeerConnection.IceServer.builder(it)
|
||||
.setUsername("xxxxx")
|
||||
.setPassword("xxxxx")
|
||||
.createIceServer()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
peerConnectionManager.createPeerConnection(videoCapturer, iceServers)
|
||||
peerConnectionManager.startCall()
|
||||
}
|
||||
// PeerConnectionFactory.initialize(PeerConnectionFactory
|
||||
// .InitializationOptions.builder(applicationContext)
|
||||
// .createInitializationOptions()
|
||||
// )
|
||||
|
||||
// val options = PeerConnectionFactory.Options()
|
||||
// val defaultVideoEncoderFactory = DefaultVideoEncoderFactory(
|
||||
// rootEglBase!!.eglBaseContext, /* enableIntelVp8Encoder */
|
||||
// true, /* enableH264HighProfile */
|
||||
// true)
|
||||
// val defaultVideoDecoderFactory = DefaultVideoDecoderFactory(rootEglBase!!.eglBaseContext)
|
||||
//
|
||||
// peerConnectionFactory = PeerConnectionFactory.builder()
|
||||
// .setOptions(options)
|
||||
// .setVideoEncoderFactory(defaultVideoEncoderFactory)
|
||||
// .setVideoDecoderFactory(defaultVideoDecoderFactory)
|
||||
// .createPeerConnectionFactory()
|
||||
|
||||
// val cameraIterator = if (Camera2Enumerator.isSupported(this)) Camera2Enumerator(this) else Camera1Enumerator(false)
|
||||
// val frontCamera = cameraIterator.deviceNames
|
||||
// ?.firstOrNull { cameraIterator.isFrontFacing(it) }
|
||||
// ?: cameraIterator.deviceNames?.first()
|
||||
// ?: return true
|
||||
// val videoCapturer = cameraIterator.createCapturer(frontCamera, null)
|
||||
//
|
||||
// // Following instruction here: https://stackoverflow.com/questions/55085726/webrtc-create-peerconnectionfactory-object
|
||||
// val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext)
|
||||
//
|
||||
// val videoSource = peerConnectionFactory?.createVideoSource(videoCapturer.isScreencast)
|
||||
// videoCapturer.initialize(surfaceTextureHelper, this, videoSource!!.capturerObserver)
|
||||
// videoCapturer.startCapture(1280, 720, 30)
|
||||
//
|
||||
//
|
||||
// val localVideoTrack = peerConnectionFactory?.createVideoTrack("ARDAMSv0", videoSource)
|
||||
//
|
||||
// // create a local audio track
|
||||
// val audioSource = peerConnectionFactory?.createAudioSource(DEFAULT_AUDIO_CONSTRAINTS)
|
||||
// val audioTrack = peerConnectionFactory?.createAudioTrack("ARDAMSa0", audioSource)
|
||||
|
||||
pipRenderer.setMirror(true)
|
||||
// localVideoTrack?.addSink(pipRenderer)
|
||||
|
||||
/*
|
||||
{
|
||||
"username": "1586847781:@valere35:matrix.org",
|
||||
"password": "ZzbqbqfT9O2G3WpCpesdts2lyns=",
|
||||
"ttl": 86400.0,
|
||||
"uris": ["turn:turn.matrix.org:3478?transport=udp", "turn:turn.matrix.org:3478?transport=tcp", "turns:turn.matrix.org:443?transport=tcp"]
|
||||
}
|
||||
*/
|
||||
|
||||
// val iceServers = ArrayList<PeerConnection.IceServer>().apply {
|
||||
// listOf("turn:turn.matrix.org:3478?transport=udp", "turn:turn.matrix.org:3478?transport=tcp", "turns:turn.matrix.org:443?transport=tcp").forEach {
|
||||
// add(
|
||||
// PeerConnection.IceServer.builder(it)
|
||||
// .setUsername("1586847781:@valere35:matrix.org")
|
||||
// .setPassword("ZzbqbqfT9O2G3WpCpesdts2lyns=")
|
||||
// .createIceServer()
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// val iceCandidateSource: PublishSubject<IceCandidate> = PublishSubject.create()
|
||||
//
|
||||
// iceCandidateSource
|
||||
// .buffer(400, TimeUnit.MILLISECONDS)
|
||||
// .subscribe {
|
||||
// // omit empty :/
|
||||
// if (it.isNotEmpty()) {
|
||||
// callViewModel.handle(VectorCallViewActions.AddLocalIceCandidate(it))
|
||||
// }
|
||||
// }
|
||||
// .disposeOnDestroy()
|
||||
//
|
||||
// peerConnection = peerConnectionFactory?.createPeerConnection(
|
||||
// iceServers,
|
||||
// object : PeerConnectionObserverAdapter() {
|
||||
// override fun onIceCandidate(p0: IceCandidate?) {
|
||||
// p0?.let {
|
||||
// iceCandidateSource.onNext(it)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onAddStream(mediaStream: MediaStream?) {
|
||||
// runOnUiThread {
|
||||
// mediaStream?.videoTracks?.firstOrNull()?.let { videoTrack ->
|
||||
// remoteVideoTrack = videoTrack
|
||||
// remoteVideoTrack?.setEnabled(true)
|
||||
// remoteVideoTrack?.addSink(fullscreenRenderer)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onRemoveStream(mediaStream: MediaStream?) {
|
||||
// remoteVideoTrack = null
|
||||
// }
|
||||
//
|
||||
// override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) {
|
||||
// if (p0 == PeerConnection.IceConnectionState.DISCONNECTED) {
|
||||
// // TODO prompt something?
|
||||
// finish()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
//
|
||||
// val localMediaStream = peerConnectionFactory?.createLocalMediaStream("ARDAMS") // magic value?
|
||||
// localMediaStream?.addTrack(localVideoTrack)
|
||||
// localMediaStream?.addTrack(audioTrack)
|
||||
//
|
||||
// val constraints = MediaConstraints()
|
||||
// constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
|
||||
// constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
|
||||
//
|
||||
// peerConnection?.addStream(localMediaStream)
|
||||
//
|
||||
// peerConnection?.createOffer(object : SdpObserver {
|
||||
// override fun onSetFailure(p0: String?) {
|
||||
// Timber.v("## VOIP onSetFailure $p0")
|
||||
// }
|
||||
//
|
||||
// override fun onSetSuccess() {
|
||||
// Timber.v("## VOIP onSetSuccess")
|
||||
// }
|
||||
//
|
||||
// override fun onCreateSuccess(sessionDescription: SessionDescription) {
|
||||
// Timber.v("## VOIP onCreateSuccess $sessionDescription")
|
||||
// peerConnection?.setLocalDescription(object : SdpObserverAdapter() {
|
||||
// override fun onSetSuccess() {
|
||||
// callViewModel.handle(VectorCallViewActions.SendOffer(sessionDescription))
|
||||
// }
|
||||
// }, sessionDescription)
|
||||
// }
|
||||
//
|
||||
// override fun onCreateFailure(p0: String?) {
|
||||
// Timber.v("## VOIP onCreateFailure $p0")
|
||||
// }
|
||||
// }, constraints)
|
||||
iceCandidateSource
|
||||
.buffer(400, TimeUnit.MILLISECONDS)
|
||||
.subscribe {
|
||||
// omit empty :/
|
||||
if (it.isNotEmpty()) {
|
||||
callViewModel.handle(VectorCallViewActions.AddLocalIceCandidate(it))
|
||||
}
|
||||
}
|
||||
.disposeOnDestroy()
|
||||
|
||||
peerConnectionManager.attachViewRenderers(pipRenderer, fullscreenRenderer)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
peerConnectionManager.detachRenderers()
|
||||
peerConnectionManager.listener = this
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun handleViewEvents(event: VectorCallViewEvents?) {
|
||||
when (event) {
|
||||
is VectorCallViewEvents.CallAnswered -> {
|
||||
val sdp = SessionDescription(SessionDescription.Type.ANSWER, event.content.answer.sdp)
|
||||
peerConnectionManager.answerReceived("", sdp)
|
||||
// peerConnection?.setRemoteDescription(object : SdpObserverAdapter() {}, sdp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @TargetApi(17)
|
||||
// private fun getDisplayMetrics(): DisplayMetrics? {
|
||||
// val displayMetrics = DisplayMetrics()
|
||||
// val windowManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
// windowManager.defaultDisplay.getRealMetrics(displayMetrics)
|
||||
// return displayMetrics
|
||||
// }
|
||||
|
||||
// @TargetApi(21)
|
||||
// private fun startScreenCapture() {
|
||||
// val mediaProjectionManager: MediaProjectionManager = application.getSystemService(
|
||||
// Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
// startActivityForResult(
|
||||
// mediaProjectionManager.createScreenCaptureIntent(), CAPTURE_PERMISSION_REQUEST_CODE)
|
||||
// }
|
||||
//
|
||||
// override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
// if (requestCode != CAPTURE_PERMISSION_REQUEST_CODE) {
|
||||
// super.onActivityResult(requestCode, resultCode, data)
|
||||
// }
|
||||
//// mediaProjectionPermissionResultCode = resultCode;
|
||||
//// mediaProjectionPermissionResultData = data;
|
||||
//// startCall();
|
||||
// }
|
||||
|
||||
companion object {
|
||||
|
||||
private const val CAPTURE_PERMISSION_REQUEST_CODE = 1
|
||||
|
||||
// private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints().apply {
|
||||
// // add all existing audio filters to avoid having echos
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "true"))
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation2", "true"))
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googDAEchoCancellation", "true"))
|
||||
//
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googTypingNoiseDetection", "true"))
|
||||
//
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "true"))
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl2", "true"))
|
||||
//
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression", "true"))
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression2", "true"))
|
||||
//
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googAudioMirroring", "false"))
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googHighpassFilter", "true"))
|
||||
// }
|
||||
|
||||
fun newIntent(context: Context, signalingRoomId: String): Intent {
|
||||
return Intent(context, VectorCallActivity::class.java).apply {
|
||||
putExtra(MvRx.KEY_ARG, CallArgs(roomId = signalingRoomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addLocalIceCandidate(candidates: IceCandidate) {
|
||||
iceCandidateSource.onNext(candidates)
|
||||
}
|
||||
|
||||
override fun addRemoteVideoTrack(videoTrack: VideoTrack) {
|
||||
runOnUiThread {
|
||||
videoTrack.setEnabled(true)
|
||||
videoTrack.addSink(fullscreenRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addLocalVideoTrack(videoTrack: VideoTrack) {
|
||||
runOnUiThread {
|
||||
videoTrack.addSink(pipRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeRemoteVideoStream(mediaStream: MediaStream) {
|
||||
}
|
||||
|
||||
override fun onDisconnect() {
|
||||
}
|
||||
|
||||
override fun sendOffer(sessionDescription: SessionDescription) {
|
||||
callViewModel.handle(VectorCallViewActions.SendOffer(sessionDescription))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.call
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.call.CallsListener
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallAnswerContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
import im.vector.matrix.android.internal.util.awaitCallback
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.platform.VectorViewEvents
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.SessionDescription
|
||||
import java.util.UUID
|
||||
|
||||
data class VectorCallViewState(
|
||||
val callId: String? = null,
|
||||
val roomId: String = ""
|
||||
) : MvRxState
|
||||
|
||||
sealed class VectorCallViewActions : VectorViewModelAction {
|
||||
|
||||
data class SendOffer(val sdp: SessionDescription) : VectorCallViewActions()
|
||||
data class AddLocalIceCandidate(val iceCandidates: List<IceCandidate>) : VectorCallViewActions()
|
||||
}
|
||||
|
||||
sealed class VectorCallViewEvents : VectorViewEvents {
|
||||
|
||||
data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
|
||||
}
|
||||
|
||||
class VectorCallViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: VectorCallViewState,
|
||||
val session: Session
|
||||
) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) {
|
||||
|
||||
private val callServiceListener: CallsListener = object : CallsListener {
|
||||
override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
|
||||
withState { state ->
|
||||
if (callAnswerContent.callId == state.callId) {
|
||||
_viewEvents.post(VectorCallViewEvents.CallAnswered(callAnswerContent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallInviteReceived(signalingRoomId: String, callInviteContent: CallInviteContent) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
session.callService().addCallListener(callServiceListener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
session.callService().removeCallListener(callServiceListener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
override fun handle(action: VectorCallViewActions) = withState { state ->
|
||||
when (action) {
|
||||
is VectorCallViewActions.SendOffer -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
awaitCallback<String> {
|
||||
val callId = state.callId ?: UUID.randomUUID().toString().also {
|
||||
setState {
|
||||
copy(callId = it)
|
||||
}
|
||||
}
|
||||
session.callService().sendOfferSdp(callId, state.roomId, action.sdp, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
is VectorCallViewActions.AddLocalIceCandidate -> {
|
||||
viewModelScope.launch {
|
||||
session.callService().sendLocalIceCandidates(state.callId ?: "", state.roomId, action.iceCandidates)
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: VectorCallViewState): VectorCallViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<VectorCallViewModel, VectorCallViewState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: VectorCallViewState): VectorCallViewModel? {
|
||||
val callActivity: VectorCallActivity = viewModelContext.activity()
|
||||
return callActivity.viewModelFactory.create(state)
|
||||
}
|
||||
|
||||
override fun initialState(viewModelContext: ViewModelContext): VectorCallViewState? {
|
||||
val args: CallArgs = viewModelContext.args()
|
||||
return VectorCallViewState(roomId = args.roomId)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.call
|
||||
|
||||
import android.os.Build
|
||||
import android.telecom.Connection
|
||||
import android.telecom.ConnectionRequest
|
||||
import android.telecom.ConnectionService
|
||||
import android.telecom.PhoneAccountHandle
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
/**
|
||||
* No active calls in other apps
|
||||
*
|
||||
*To answer incoming calls when there are no active calls in other apps, follow these steps:
|
||||
*
|
||||
* <pre>
|
||||
* * Your app receives a new incoming call using its usual mechanisms.
|
||||
* - Use the addNewIncomingCall(PhoneAccountHandle, Bundle) method to inform the telecom subsystem about the new incoming call.
|
||||
* - The telecom subsystem binds to your app's ConnectionService implementation and requests a new instance of the
|
||||
* Connection class representing the new incoming call using the onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest) method.
|
||||
* - The telecom subsystem informs your app that it should show its incoming call user interface using the onShowIncomingCallUi() method.
|
||||
* - Your app shows its incoming UI using a notification with an associated full-screen intent. For more information, see onShowIncomingCallUi().
|
||||
* - Call the setActive() method if the user accepts the incoming call, or setDisconnected(DisconnectCause) specifying REJECTED as
|
||||
* the parameter followed by a call to the destroy() method if the user rejects the incoming call.
|
||||
*</pre>
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.M) class VectorConnectionService : ConnectionService() {
|
||||
|
||||
/**
|
||||
* The telecom subsystem calls this method in response to your app calling placeCall(Uri, Bundle) to create a new outgoing call
|
||||
*/
|
||||
override fun onCreateOutgoingConnection(connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest?): Connection? {
|
||||
val callId = request?.address?.encodedQuery ?: return null
|
||||
val roomId = request.extras.getString("MX_CALL_ROOM_ID") ?: return null
|
||||
return CallConnection(applicationContext, roomId, callId)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,372 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.call
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.telecom.PhoneAccount
|
||||
import android.telecom.PhoneAccountHandle
|
||||
import android.telecom.TelecomManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import im.vector.matrix.android.api.session.call.CallsListener
|
||||
import im.vector.matrix.android.api.session.call.EglUtils
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallAnswerContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
import im.vector.riotx.BuildConfig
|
||||
import im.vector.riotx.R
|
||||
import org.webrtc.AudioSource
|
||||
import org.webrtc.AudioTrack
|
||||
import org.webrtc.DefaultVideoDecoderFactory
|
||||
import org.webrtc.DefaultVideoEncoderFactory
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.MediaConstraints
|
||||
import org.webrtc.MediaStream
|
||||
import org.webrtc.PeerConnection
|
||||
import org.webrtc.PeerConnectionFactory
|
||||
import org.webrtc.SdpObserver
|
||||
import org.webrtc.SessionDescription
|
||||
import org.webrtc.SurfaceTextureHelper
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
import org.webrtc.VideoCapturer
|
||||
import org.webrtc.VideoSource
|
||||
import org.webrtc.VideoTrack
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Manage peerConnectionFactory & Peer connections outside of activity lifecycle to resist configuration changes
|
||||
* Use app context
|
||||
*/
|
||||
@Singleton
|
||||
class WebRtcPeerConnectionManager @Inject constructor(
|
||||
private val context: Context
|
||||
) : CallsListener {
|
||||
|
||||
interface Listener {
|
||||
fun addLocalIceCandidate(candidates: IceCandidate)
|
||||
fun addRemoteVideoTrack(videoTrack: VideoTrack)
|
||||
fun addLocalVideoTrack(videoTrack: VideoTrack)
|
||||
fun removeRemoteVideoStream(mediaStream: MediaStream)
|
||||
fun onDisconnect()
|
||||
fun sendOffer(sessionDescription: SessionDescription)
|
||||
}
|
||||
|
||||
var phoneAccountHandle: PhoneAccountHandle? = null
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val componentName = ComponentName(BuildConfig.APPLICATION_ID, VectorConnectionService::class.java.name)
|
||||
val appName = context.getString(R.string.app_name)
|
||||
phoneAccountHandle = PhoneAccountHandle(componentName, appName)
|
||||
val phoneAccount = PhoneAccount.Builder(phoneAccountHandle, appName)
|
||||
.setIcon(Icon.createWithResource(context, R.drawable.riotx_logo))
|
||||
.build()
|
||||
ContextCompat.getSystemService(context, TelecomManager::class.java)
|
||||
?.registerPhoneAccount(phoneAccount)
|
||||
} else {
|
||||
// ignore?
|
||||
}
|
||||
}
|
||||
|
||||
var listener: Listener? = null
|
||||
|
||||
// *Comments copied from webrtc demo app*
|
||||
// Executor thread is started once and is used for all
|
||||
// peer connection API calls to ensure new peer connection factory is
|
||||
// created on the same thread as previously destroyed factory.
|
||||
private val executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
private val rootEglBase by lazy { EglUtils.rootEglBase }
|
||||
|
||||
private var peerConnectionFactory: PeerConnectionFactory? = null
|
||||
|
||||
private var peerConnection: PeerConnection? = null
|
||||
|
||||
private var remoteVideoTrack: VideoTrack? = null
|
||||
private var localVideoTrack: VideoTrack? = null
|
||||
|
||||
private var videoSource: VideoSource? = null
|
||||
private var audioSource: AudioSource? = null
|
||||
private var audioTrack: AudioTrack? = null
|
||||
|
||||
private var videoCapturer: VideoCapturer? = null
|
||||
|
||||
var localSurfaceRenderer: WeakReference<SurfaceViewRenderer>? = null
|
||||
var remoteSurfaceRenderer: WeakReference<SurfaceViewRenderer>? = null
|
||||
|
||||
fun createPeerConnectionFactory() {
|
||||
executor.execute {
|
||||
if (peerConnectionFactory == null) {
|
||||
Timber.v("## VOIP createPeerConnectionFactory")
|
||||
val eglBaseContext = rootEglBase?.eglBaseContext ?: return@execute Unit.also {
|
||||
Timber.e("## VOIP No EGL BASE")
|
||||
}
|
||||
|
||||
Timber.v("## VOIP PeerConnectionFactory.initialize")
|
||||
PeerConnectionFactory.initialize(PeerConnectionFactory
|
||||
.InitializationOptions.builder(context.applicationContext)
|
||||
.createInitializationOptions()
|
||||
)
|
||||
|
||||
val options = PeerConnectionFactory.Options()
|
||||
val defaultVideoEncoderFactory = DefaultVideoEncoderFactory(
|
||||
eglBaseContext,
|
||||
/* enableIntelVp8Encoder */
|
||||
true,
|
||||
/* enableH264HighProfile */
|
||||
true)
|
||||
val defaultVideoDecoderFactory = DefaultVideoDecoderFactory(eglBaseContext)
|
||||
|
||||
|
||||
Timber.v("## VOIP PeerConnectionFactory.createPeerConnectionFactory ...")
|
||||
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||
.setOptions(options)
|
||||
.setVideoEncoderFactory(defaultVideoEncoderFactory)
|
||||
.setVideoDecoderFactory(defaultVideoDecoderFactory)
|
||||
.createPeerConnectionFactory()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createPeerConnection(videoCapturer: VideoCapturer, iceServers: List<PeerConnection.IceServer>) {
|
||||
executor.execute {
|
||||
Timber.v("## VOIP PeerConnectionFactory.createPeerConnection ${peerConnectionFactory}...")
|
||||
// Following instruction here: https://stackoverflow.com/questions/55085726/webrtc-create-peerconnectionfactory-object
|
||||
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext)
|
||||
|
||||
videoSource = peerConnectionFactory?.createVideoSource(videoCapturer.isScreencast)
|
||||
Timber.v("## VOIP Local video source created")
|
||||
videoCapturer.initialize(surfaceTextureHelper, context.applicationContext, videoSource!!.capturerObserver)
|
||||
videoCapturer.startCapture(1280, 720, 30)
|
||||
|
||||
localVideoTrack = peerConnectionFactory?.createVideoTrack("ARDAMSv0", videoSource)?.also {
|
||||
Timber.v("## VOIP Local video track created")
|
||||
listener?.addLocalVideoTrack(it)
|
||||
// localSurfaceRenderer?.get()?.let { surface ->
|
||||
//// it.addSink(surface)
|
||||
//// }
|
||||
}
|
||||
|
||||
// create a local audio track
|
||||
Timber.v("## VOIP create local audio track")
|
||||
audioSource = peerConnectionFactory?.createAudioSource(DEFAULT_AUDIO_CONSTRAINTS)
|
||||
audioTrack = peerConnectionFactory?.createAudioTrack(AUDIO_TRACK_ID, audioSource)
|
||||
|
||||
// pipRenderer.setMirror(true)
|
||||
// localVideoTrack?.addSink(pipRenderer)
|
||||
//
|
||||
|
||||
// val iceCandidateSource: PublishSubject<IceCandidate> = PublishSubject.create()
|
||||
//
|
||||
// iceCandidateSource
|
||||
// .buffer(400, TimeUnit.MILLISECONDS)
|
||||
// .subscribe {
|
||||
// // omit empty :/
|
||||
// if (it.isNotEmpty()) {
|
||||
// listener.addLocalIceCandidate()
|
||||
// callViewModel.handle(VectorCallViewActions.AddLocalIceCandidate(it))
|
||||
// }
|
||||
// }
|
||||
// .disposeOnDestroy()
|
||||
|
||||
Timber.v("## VOIP creating peer connection... ")
|
||||
peerConnection = peerConnectionFactory?.createPeerConnection(
|
||||
iceServers,
|
||||
object : PeerConnectionObserverAdapter() {
|
||||
override fun onIceCandidate(p0: IceCandidate?) {
|
||||
Timber.v("## VOIP onIceCandidate local $p0")
|
||||
p0?.let {
|
||||
// iceCandidateSource.onNext(it)
|
||||
listener?.addLocalIceCandidate(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddStream(mediaStream: MediaStream?) {
|
||||
Timber.v("## VOIP onAddStream remote $mediaStream")
|
||||
mediaStream?.videoTracks?.firstOrNull()?.let {
|
||||
listener?.addRemoteVideoTrack(it)
|
||||
remoteVideoTrack = it
|
||||
// remoteSurfaceRenderer?.get()?.let { surface ->
|
||||
// it.setEnabled(true)
|
||||
// it.addSink(surface)
|
||||
// }
|
||||
}
|
||||
// runOnUiThread {
|
||||
// mediaStream?.videoTracks?.firstOrNull()?.let { videoTrack ->
|
||||
// remoteVideoTrack = videoTrack
|
||||
// remoteVideoTrack?.setEnabled(true)
|
||||
// remoteVideoTrack?.addSink(fullscreenRenderer)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onRemoveStream(mediaStream: MediaStream?) {
|
||||
mediaStream?.let {
|
||||
listener?.removeRemoteVideoStream(it)
|
||||
}
|
||||
remoteSurfaceRenderer?.get()?.let {
|
||||
remoteVideoTrack?.removeSink(it)
|
||||
}
|
||||
remoteVideoTrack = null
|
||||
}
|
||||
|
||||
override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) {
|
||||
Timber.v("## VOIP onIceConnectionChange $p0")
|
||||
if (p0 == PeerConnection.IceConnectionState.DISCONNECTED) {
|
||||
listener?.onDisconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val localMediaStream = peerConnectionFactory?.createLocalMediaStream("ARDAMS") // magic value?
|
||||
localMediaStream?.addTrack(localVideoTrack)
|
||||
localMediaStream?.addTrack(audioTrack)
|
||||
|
||||
// val constraints = MediaConstraints()
|
||||
// constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
|
||||
// constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
|
||||
|
||||
Timber.v("## VOIP add local stream to peer connection")
|
||||
peerConnection?.addStream(localMediaStream)
|
||||
}
|
||||
}
|
||||
|
||||
fun answerReceived(callId: String, answerSdp: SessionDescription) {
|
||||
executor.execute {
|
||||
Timber.v("## answerReceived $callId")
|
||||
peerConnection?.setRemoteDescription(object : SdpObserverAdapter() {}, answerSdp)
|
||||
}
|
||||
}
|
||||
|
||||
fun startCall() {
|
||||
executor.execute {
|
||||
val constraints = MediaConstraints()
|
||||
constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
|
||||
constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
|
||||
|
||||
Timber.v("## VOIP creating offer...")
|
||||
peerConnection?.createOffer(object : SdpObserver {
|
||||
override fun onSetFailure(p0: String?) {
|
||||
Timber.v("## VOIP onSetFailure $p0")
|
||||
}
|
||||
|
||||
override fun onSetSuccess() {
|
||||
Timber.v("## VOIP onSetSuccess")
|
||||
}
|
||||
|
||||
override fun onCreateSuccess(sessionDescription: SessionDescription) {
|
||||
Timber.v("## VOIP onCreateSuccess $sessionDescription")
|
||||
peerConnection?.setLocalDescription(object : SdpObserverAdapter() {
|
||||
override fun onSetSuccess() {
|
||||
listener?.sendOffer(sessionDescription)
|
||||
//callViewModel.handle(VectorCallViewActions.SendOffer(sessionDescription))
|
||||
}
|
||||
}, sessionDescription)
|
||||
}
|
||||
|
||||
override fun onCreateFailure(p0: String?) {
|
||||
Timber.v("## VOIP onCreateFailure $p0")
|
||||
}
|
||||
}, constraints)
|
||||
}
|
||||
}
|
||||
|
||||
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer, remoteViewRenderer: SurfaceViewRenderer) {
|
||||
localVideoTrack?.addSink(localViewRenderer)
|
||||
remoteVideoTrack?.let {
|
||||
it.setEnabled(true)
|
||||
it.addSink(remoteViewRenderer)
|
||||
}
|
||||
localSurfaceRenderer = WeakReference(localViewRenderer)
|
||||
remoteSurfaceRenderer = WeakReference(remoteViewRenderer)
|
||||
}
|
||||
|
||||
fun detachRenderers() {
|
||||
localSurfaceRenderer?.get()?.let {
|
||||
localVideoTrack?.removeSink(it)
|
||||
}
|
||||
remoteSurfaceRenderer?.get()?.let {
|
||||
remoteVideoTrack?.removeSink(it)
|
||||
}
|
||||
localSurfaceRenderer = null
|
||||
remoteSurfaceRenderer = null
|
||||
}
|
||||
|
||||
fun close() {
|
||||
executor.execute {
|
||||
peerConnectionFactory?.stopAecDump()
|
||||
peerConnectionFactory = null
|
||||
audioSource?.dispose()
|
||||
videoSource?.dispose()
|
||||
peerConnection?.dispose()
|
||||
peerConnection = null
|
||||
videoCapturer?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val AUDIO_TRACK_ID = "ARDAMSa0"
|
||||
|
||||
private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints().apply {
|
||||
// add all existing audio filters to avoid having echos
|
||||
mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "true"))
|
||||
mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation2", "true"))
|
||||
mandatory.add(MediaConstraints.KeyValuePair("googDAEchoCancellation", "true"))
|
||||
|
||||
mandatory.add(MediaConstraints.KeyValuePair("googTypingNoiseDetection", "true"))
|
||||
|
||||
mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "true"))
|
||||
mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl2", "true"))
|
||||
|
||||
mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression", "true"))
|
||||
mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression2", "true"))
|
||||
|
||||
mandatory.add(MediaConstraints.KeyValuePair("googAudioMirroring", "false"))
|
||||
mandatory.add(MediaConstraints.KeyValuePair("googHighpassFilter", "true"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallInviteReceived(signalingRoomId: String, callInviteContent: CallInviteContent) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
ContextCompat.getSystemService(context, TelecomManager::class.java)?.let { telecomManager ->
|
||||
phoneAccountHandle?.let { phoneAccountHandle ->
|
||||
telecomManager.addNewIncomingCall(
|
||||
phoneAccountHandle,
|
||||
Bundle().apply {
|
||||
putString("MX_CALL_ROOM_ID", signalingRoomId)
|
||||
putString("MX_CALL_CALL_ID", callInviteContent.callId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +68,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||
|
||||
object ClearSendQueue : RoomDetailAction()
|
||||
object ResendAll : RoomDetailAction()
|
||||
object StartCall : RoomDetailAction()
|
||||
|
||||
data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction()
|
||||
data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction()
|
||||
|
|
|
@ -127,6 +127,7 @@ import im.vector.riotx.features.attachments.ContactAttachment
|
|||
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewActivity
|
||||
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs
|
||||
import im.vector.riotx.features.attachments.toGroupedContentAttachmentData
|
||||
import im.vector.riotx.features.call.VectorCallActivity
|
||||
import im.vector.riotx.features.command.Command
|
||||
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
|
||||
import im.vector.riotx.features.crypto.util.toImageRes
|
||||
|
@ -479,6 +480,17 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
if (item.itemId == R.id.resend_all) {
|
||||
roomDetailViewModel.handle(RoomDetailAction.ResendAll)
|
||||
return true
|
||||
}
|
||||
if (item.itemId == R.id.voip_call) {
|
||||
VectorCallActivity.newIntent(requireContext(), roomDetailArgs.roomId).let {
|
||||
startActivity(it)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun displayDisabledIntegrationDialog() {
|
||||
|
|
|
@ -66,6 +66,7 @@ import im.vector.riotx.core.platform.VectorViewModel
|
|||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.core.resources.UserPreferencesProvider
|
||||
import im.vector.riotx.core.utils.subscribeLogError
|
||||
import im.vector.riotx.features.call.VectorCallActivity
|
||||
import im.vector.riotx.features.command.CommandParser
|
||||
import im.vector.riotx.features.command.ParsedCommand
|
||||
import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||
|
@ -372,6 +373,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
R.id.resend_all -> timeline.failedToDeliverEventCount() > 0
|
||||
R.id.clear_all -> timeline.failedToDeliverEventCount() > 0
|
||||
R.id.open_matrix_apps -> true
|
||||
R.id.voip_call -> room.roomSummary()?.isDirect == true && room.roomSummary()?.joinedMembersCount == 2
|
||||
else -> false
|
||||
}
|
||||
|
||||
|
|
|
@ -239,7 +239,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
|
|||
return when (type) {
|
||||
EventType.CALL_INVITE -> {
|
||||
val content = event.getClearContent().toModel<CallInviteContent>() ?: return null
|
||||
val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO
|
||||
val isVideoCall = content.offer?.sdp == CallInviteContent.Offer.SDP_VIDEO
|
||||
return if (isVideoCall) {
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_placed_video_call_by_you)
|
||||
|
|
14
vector/src/main/res/drawable/ic_phone.xml
Normal file
14
vector/src/main/res/drawable/ic_phone.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M21.9994,16.9738V19.9846C22.0017,20.5498 21.7651,21.0898 21.3478,21.4718C20.9305,21.8539 20.3712,22.0427 19.8072,21.9918C16.7128,21.6563 13.7404,20.601 11.1289,18.9108C8.6992,17.3699 6.6393,15.3141 5.0953,12.8892C3.3959,10.271 2.3382,7.2901 2.0082,4.188C1.9574,3.6268 2.1452,3.0702 2.5258,2.6541C2.9064,2.2379 3.4448,2.0006 4.0093,2.0001H7.0261C8.0356,1.9902 8.896,2.7287 9.0373,3.7263C9.1646,4.6898 9.4007,5.6359 9.7412,6.5464C10.0175,7.2799 9.8407,8.1068 9.2887,8.664L8.0116,9.9386C9.4431,12.4512 11.5276,14.5315 14.0451,15.9602L15.3222,14.6856C15.8805,14.1346 16.7091,13.9583 17.444,14.234C18.3564,14.5738 19.3043,14.8094 20.2697,14.9365C21.2809,15.0789 22.0247,15.955 21.9994,16.9738Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
33
vector/src/main/res/layout/activity_call.xml
Normal file
33
vector/src/main/res/layout/activity_call.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- tools:ignore is needed because lint thinks this can be replaced with a merge. Replacing this
|
||||
with a merge causes the fullscreen SurfaceView not to be centered. -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="MergeRootFrame">
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/fullscreen_video_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/pip_video_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="144dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/hud_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/call_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
63
vector/src/main/res/layout/fragment_call.xml
Normal file
63
vector/src/main/res/layout/fragment_call.xml
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<TextView
|
||||
android:id="@+id/contact_name_call"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_above="@+id/buttons_call_container"
|
||||
android:textSize="24sp"
|
||||
android:layout_margin="8dp"/>
|
||||
<LinearLayout
|
||||
android:id="@+id/buttons_call_container"
|
||||
android:orientation="horizontal"
|
||||
android:layout_above="@+id/capture_format_text_call"
|
||||
android:layout_alignWithParentIfMissing="true"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<ImageButton
|
||||
android:id="@+id/button_call_disconnect"
|
||||
android:background="@drawable/ic_phone"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"/>
|
||||
<ImageButton
|
||||
android:id="@+id/button_call_switch_camera"
|
||||
android:background="@drawable/ic_camera"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"/>
|
||||
<ImageButton
|
||||
android:id="@+id/button_call_scaling_mode"
|
||||
android:background="@drawable/scrolldown"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"/>
|
||||
<ImageButton
|
||||
android:id="@+id/button_call_toggle_mic"
|
||||
android:background="@android:drawable/ic_btn_speak_now"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"/>
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/capture_format_text_call"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_above="@+id/capture_format_slider_call"
|
||||
android:textSize="16sp"
|
||||
tools:text="Slide to change capture format"/>
|
||||
<SeekBar
|
||||
android:id="@+id/capture_format_slider_call"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:progress="50"
|
||||
android:layout_margin="8dp"/>
|
||||
</RelativeLayout>
|
|
@ -8,6 +8,14 @@
|
|||
android:title="@string/room_add_matrix_apps"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/voip_call"
|
||||
android:icon="@drawable/ic_phone"
|
||||
android:title="@string/call"
|
||||
android:visible="false"
|
||||
app:showAsAction="always"
|
||||
tools:visible="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/resend_all"
|
||||
android:icon="@drawable/ic_refresh_cw"
|
||||
|
|
Loading…
Reference in a new issue