mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-16 12:00:03 +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
|
// Phone number https://github.com/google/libphonenumber
|
||||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
|
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'
|
debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0'
|
||||||
releaseImplementation 'com.airbnb.okreplay:noop:1.5.0'
|
releaseImplementation 'com.airbnb.okreplay:noop:1.5.0'
|
||||||
androidTestImplementation 'com.airbnb.okreplay:espresso: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.account.AccountService
|
||||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
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.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.ContentUploadStateTracker
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
|
@ -165,6 +166,11 @@ interface Session :
|
||||||
*/
|
*/
|
||||||
fun integrationManagerService(): IntegrationManagerService
|
fun integrationManagerService(): IntegrationManagerService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the cryptoService associated with the session
|
||||||
|
*/
|
||||||
|
fun callService(): CallService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a listener to the session.
|
* Add a listener to the session.
|
||||||
* @param listener the listener to add.
|
* @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"
|
const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
|
||||||
|
|
||||||
// Call Events
|
// Call Events
|
||||||
|
|
||||||
const val CALL_INVITE = "m.call.invite"
|
const val CALL_INVITE = "m.call.invite"
|
||||||
const val CALL_CANDIDATES = "m.call.candidates"
|
const val CALL_CANDIDATES = "m.call.candidates"
|
||||||
const val CALL_ANSWER = "m.call.answer"
|
const val CALL_ANSWER = "m.call.answer"
|
||||||
|
|
|
@ -21,21 +21,42 @@ import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class CallInviteContent(
|
data class CallInviteContent(
|
||||||
@Json(name = "call_id") val callId: String,
|
|
||||||
@Json(name = "version") val version: Int,
|
/**
|
||||||
@Json(name = "lifetime") val lifetime: Int,
|
* A unique identifier for the call.
|
||||||
@Json(name = "offer") val offer: Offer
|
*/
|
||||||
|
@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)
|
@JsonClass(generateAdapter = true)
|
||||||
data class Offer(
|
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 {
|
companion object {
|
||||||
const val SDP_VIDEO = "m=video"
|
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.DefaultGetDeviceInfoTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDevicesTask
|
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.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.DefaultSendToDeviceTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSetDeviceNameTask
|
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.GetDeviceInfoTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
|
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.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.SendToDeviceTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
|
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
||||||
|
@ -251,4 +253,7 @@ internal abstract class CryptoModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
|
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.account.AccountService
|
||||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
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.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.ContentUploadStateTracker
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
|
@ -110,7 +111,8 @@ internal class DefaultSession @Inject constructor(
|
||||||
private val integrationManagerService: IntegrationManagerService,
|
private val integrationManagerService: IntegrationManagerService,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val widgetDependenciesHolder: WidgetDependenciesHolder,
|
private val widgetDependenciesHolder: WidgetDependenciesHolder,
|
||||||
private val shieldTrustUpdater: ShieldTrustUpdater)
|
private val shieldTrustUpdater: ShieldTrustUpdater,
|
||||||
|
private val callService: Lazy<CallService>)
|
||||||
: Session,
|
: Session,
|
||||||
RoomService by roomService.get(),
|
RoomService by roomService.get(),
|
||||||
RoomDirectoryService by roomDirectoryService.get(),
|
RoomDirectoryService by roomDirectoryService.get(),
|
||||||
|
@ -245,6 +247,8 @@ internal class DefaultSession @Inject constructor(
|
||||||
|
|
||||||
override fun integrationManagerService() = integrationManagerService
|
override fun integrationManagerService() = integrationManagerService
|
||||||
|
|
||||||
|
override fun callService(): CallService = callService.get()
|
||||||
|
|
||||||
override fun addListener(listener: Session.Listener) {
|
override fun addListener(listener: Session.Listener) {
|
||||||
sessionListeners.addListener(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.InitialSyncProgressService
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
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.homeserver.HomeServerCapabilitiesService
|
||||||
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
|
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
|
||||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
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.NetworkConnectivityChecker
|
||||||
import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy
|
import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy
|
||||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||||
import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor
|
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
|
||||||
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
import im.vector.matrix.android.internal.session.call.CallEventObserver
|
||||||
import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider
|
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.group.GroupSummaryUpdater
|
||||||
import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService
|
import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService
|
||||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||||
|
@ -245,7 +246,11 @@ internal abstract class SessionModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindRoomTombstoneEventLiveObserver(observer: RoomTombstoneEventLiveObserver): LiveEntityObserver
|
abstract fun bindCallEventObserver(callEventObserver: CallEventObserver): LiveEntityObserver
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoSet
|
||||||
|
abstract fun bindRoomTombstoneEventLiveObserver(roomTombstoneEventLiveObserver: RoomTombstoneEventLiveObserver): LiveEntityObserver
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
|
@ -269,4 +274,7 @@ internal abstract class SessionModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindSharedSecretStorageService(service: DefaultSharedSecretStorageService): SharedSecretStorageService
|
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.api.session.room.RoomService
|
||||||
import im.vector.matrix.android.internal.session.DefaultFileService
|
import im.vector.matrix.android.internal.session.DefaultFileService
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
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.DefaultGetRoomIdByAliasTask
|
||||||
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
|
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
|
||||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
||||||
|
@ -201,4 +203,8 @@ internal abstract class RoomModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindDeleteTagFromRoomTask(task: DefaultDeleteTagFromRoomTask): DeleteTagFromRoomTask
|
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 localEchoEventFactory: LocalEchoEventFactory,
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoService: CryptoService,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val localEchoRepository: LocalEchoRepository
|
private val localEchoRepository: LocalEchoRepository,
|
||||||
|
private val roomEventSender: RoomEventSender
|
||||||
) : SendService {
|
) : SendService {
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
|
@ -111,20 +112,6 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
.let { sendEvent(it) }
|
.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>,
|
override fun sendMedias(attachments: List<ContentAttachmentData>,
|
||||||
compressBeforeSending: Boolean,
|
compressBeforeSending: Boolean,
|
||||||
roomIds: Set<String>): Cancelable {
|
roomIds: Set<String>): Cancelable {
|
||||||
|
@ -269,6 +256,10 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
return cancelableBag
|
return cancelableBag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sendEvent(event: Event): Cancelable {
|
||||||
|
return roomEventSender.sendEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
private fun createLocalEcho(event: Event) {
|
private fun createLocalEcho(event: Event) {
|
||||||
localEchoEventFactory.createLocalEcho(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'
|
implementation 'com.github.BillCarsonFr:JsonViewer:0.5'
|
||||||
|
|
||||||
|
// TODO meant for development purposes only
|
||||||
|
implementation 'org.webrtc:google-webrtc:1.0.+'
|
||||||
|
|
||||||
// QR-code
|
// QR-code
|
||||||
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170
|
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170
|
||||||
implementation 'com.google.zxing:core:3.3.3'
|
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.allGranted
|
||||||
import im.vector.riotx.core.utils.checkPermissions
|
import im.vector.riotx.core.utils.checkPermissions
|
||||||
import im.vector.riotx.core.utils.toast
|
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.debug.sas.DebugSasEmojiActivity
|
||||||
import im.vector.riotx.features.qrcode.QrCodeScannerActivity
|
import im.vector.riotx.features.qrcode.QrCodeScannerActivity
|
||||||
import kotlinx.android.synthetic.debug.activity_debug_menu.*
|
import kotlinx.android.synthetic.debug.activity_debug_menu.*
|
||||||
|
@ -183,7 +184,8 @@ class DebugMenuActivity : VectorBaseActivity() {
|
||||||
@OnClick(R.id.debug_scan_qr_code)
|
@OnClick(R.id.debug_scan_qr_code)
|
||||||
fun scanQRCode() {
|
fun scanQRCode() {
|
||||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
|
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.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<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 -->
|
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
|
||||||
<!-- Tell that the Camera is not mandatory to install the application -->
|
<!-- Tell that the Camera is not mandatory to install the application -->
|
||||||
<uses-feature
|
<uses-feature
|
||||||
|
@ -172,6 +180,7 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".features.attachments.preview.AttachmentsPreviewActivity"
|
android:name=".features.attachments.preview.AttachmentsPreviewActivity"
|
||||||
android:theme="@style/AppTheme.AttachmentsPreview" />
|
android:theme="@style/AppTheme.AttachmentsPreview" />
|
||||||
|
<activity android:name=".features.call.VectorCallActivity" />
|
||||||
|
|
||||||
<activity android:name=".features.terms.ReviewTermsActivity" />
|
<activity android:name=".features.terms.ReviewTermsActivity" />
|
||||||
<activity android:name=".features.widgets.WidgetActivity" />
|
<activity android:name=".features.widgets.WidgetActivity" />
|
||||||
|
@ -186,6 +195,13 @@
|
||||||
android:name=".core.services.VectorSyncService"
|
android:name=".core.services.VectorSyncService"
|
||||||
android:exported="false" />
|
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 -->
|
<!-- Receivers -->
|
||||||
|
|
||||||
<!-- Exported false, should only be accessible from this app!! -->
|
<!-- 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.di.VectorComponent
|
||||||
import im.vector.riotx.core.extensions.configureAndStart
|
import im.vector.riotx.core.extensions.configureAndStart
|
||||||
import im.vector.riotx.core.rx.RxConfig
|
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.configuration.VectorConfiguration
|
||||||
import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
|
import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
|
||||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||||
|
@ -80,6 +81,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
||||||
@Inject lateinit var appStateHandler: AppStateHandler
|
@Inject lateinit var appStateHandler: AppStateHandler
|
||||||
@Inject lateinit var rxConfig: RxConfig
|
@Inject lateinit var rxConfig: RxConfig
|
||||||
@Inject lateinit var popupAlertManager: PopupAlertManager
|
@Inject lateinit var popupAlertManager: PopupAlertManager
|
||||||
|
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||||
|
|
||||||
lateinit var vectorComponent: VectorComponent
|
lateinit var vectorComponent: VectorComponent
|
||||||
private var fontThreadHandler: Handler? = null
|
private var fontThreadHandler: Handler? = null
|
||||||
|
|
||||||
|
@ -122,6 +125,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
||||||
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
|
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
|
||||||
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
|
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
|
||||||
lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
|
lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
|
||||||
|
lastAuthenticatedSession.callService().addCallListener(webRtcPeerConnectionManager)
|
||||||
}
|
}
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
|
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||||
|
|
|
@ -24,6 +24,7 @@ import dagger.Component
|
||||||
import im.vector.riotx.core.error.ErrorFormatter
|
import im.vector.riotx.core.error.ErrorFormatter
|
||||||
import im.vector.riotx.core.preference.UserAvatarPreference
|
import im.vector.riotx.core.preference.UserAvatarPreference
|
||||||
import im.vector.riotx.features.MainActivity
|
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.createdirect.CreateDirectRoomActivity
|
||||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||||
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
|
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
|
||||||
|
@ -130,6 +131,7 @@ interface ScreenComponent {
|
||||||
fun inject(activity: InviteUsersToRoomActivity)
|
fun inject(activity: InviteUsersToRoomActivity)
|
||||||
fun inject(activity: ReviewTermsActivity)
|
fun inject(activity: ReviewTermsActivity)
|
||||||
fun inject(activity: WidgetActivity)
|
fun inject(activity: WidgetActivity)
|
||||||
|
fun inject(activity: VectorCallActivity)
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* BottomSheets
|
* BottomSheets
|
||||||
|
|
|
@ -31,6 +31,7 @@ import im.vector.riotx.core.error.ErrorFormatter
|
||||||
import im.vector.riotx.core.pushers.PushersManager
|
import im.vector.riotx.core.pushers.PushersManager
|
||||||
import im.vector.riotx.core.utils.AssetReader
|
import im.vector.riotx.core.utils.AssetReader
|
||||||
import im.vector.riotx.core.utils.DimensionConverter
|
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.configuration.VectorConfiguration
|
||||||
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
|
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
|
||||||
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
|
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
|
||||||
|
@ -134,6 +135,8 @@ interface VectorComponent {
|
||||||
|
|
||||||
fun reAuthHelper(): ReAuthHelper
|
fun reAuthHelper(): ReAuthHelper
|
||||||
|
|
||||||
|
fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(@BindsInstance context: Context): VectorComponent
|
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 ClearSendQueue : RoomDetailAction()
|
||||||
object ResendAll : RoomDetailAction()
|
object ResendAll : RoomDetailAction()
|
||||||
|
object StartCall : RoomDetailAction()
|
||||||
|
|
||||||
data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction()
|
data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction()
|
||||||
data class DeclineVerificationRequest(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.AttachmentsPreviewActivity
|
||||||
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs
|
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs
|
||||||
import im.vector.riotx.features.attachments.toGroupedContentAttachmentData
|
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.command.Command
|
||||||
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
|
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
|
||||||
import im.vector.riotx.features.crypto.util.toImageRes
|
import im.vector.riotx.features.crypto.util.toImageRes
|
||||||
|
@ -479,6 +480,17 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
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() {
|
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.StringProvider
|
||||||
import im.vector.riotx.core.resources.UserPreferencesProvider
|
import im.vector.riotx.core.resources.UserPreferencesProvider
|
||||||
import im.vector.riotx.core.utils.subscribeLogError
|
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.CommandParser
|
||||||
import im.vector.riotx.features.command.ParsedCommand
|
import im.vector.riotx.features.command.ParsedCommand
|
||||||
import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider
|
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.resend_all -> timeline.failedToDeliverEventCount() > 0
|
||||||
R.id.clear_all -> timeline.failedToDeliverEventCount() > 0
|
R.id.clear_all -> timeline.failedToDeliverEventCount() > 0
|
||||||
R.id.open_matrix_apps -> true
|
R.id.open_matrix_apps -> true
|
||||||
|
R.id.voip_call -> room.roomSummary()?.isDirect == true && room.roomSummary()?.joinedMembersCount == 2
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -239,7 +239,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
|
||||||
return when (type) {
|
return when (type) {
|
||||||
EventType.CALL_INVITE -> {
|
EventType.CALL_INVITE -> {
|
||||||
val content = event.getClearContent().toModel<CallInviteContent>() ?: return null
|
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) {
|
return if (isVideoCall) {
|
||||||
if (event.isSentByCurrentUser()) {
|
if (event.isSentByCurrentUser()) {
|
||||||
sp.getString(R.string.notice_placed_video_call_by_you)
|
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"
|
android:title="@string/room_add_matrix_apps"
|
||||||
app:showAsAction="never" />
|
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
|
<item
|
||||||
android:id="@+id/resend_all"
|
android:id="@+id/resend_all"
|
||||||
android:icon="@drawable/ic_refresh_cw"
|
android:icon="@drawable/ic_refresh_cw"
|
||||||
|
|
Loading…
Add table
Reference in a new issue