mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 05:31:21 +03:00
WIP | Avoid re-negociation pre-agree-upon signaling/negotiation.
This commit is contained in:
parent
435a6b2f1a
commit
9006acb66a
39 changed files with 1705 additions and 634 deletions
|
@ -16,10 +16,15 @@
|
|||
|
||||
package im.vector.matrix.android.api.extensions
|
||||
|
||||
inline fun <A> tryThis(operation: () -> A): A? {
|
||||
import timber.log.Timber
|
||||
|
||||
inline fun <A> tryThis(message: String? = null, operation: () -> A): A? {
|
||||
return try {
|
||||
operation()
|
||||
} catch (any: Throwable) {
|
||||
if (message != null) {
|
||||
Timber.e(any, message)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import im.vector.matrix.android.api.pushrules.PushRuleService
|
|||
import im.vector.matrix.android.api.session.account.AccountService
|
||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||
import im.vector.matrix.android.api.session.cache.CacheService
|
||||
import im.vector.matrix.android.api.session.call.CallService
|
||||
import im.vector.matrix.android.api.session.call.CallSignalingService
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
|
@ -155,6 +155,7 @@ interface Session :
|
|||
* Returns the identity service associated with the session
|
||||
*/
|
||||
fun identityService(): IdentityService
|
||||
fun callService(): CallSignalingService
|
||||
|
||||
/**
|
||||
* Returns the widget service associated with the session
|
||||
|
|
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.call
|
|||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
interface CallService {
|
||||
interface CallSignalingService {
|
||||
|
||||
fun getTurnServer(callback: MatrixCallback<TurnServer>): Cancelable
|
||||
|
||||
|
@ -31,4 +31,6 @@ interface CallService {
|
|||
fun addCallListener(listener: CallsListener)
|
||||
|
||||
fun removeCallListener(listener: CallsListener)
|
||||
|
||||
fun getCallWithId(callId: String) : MxCall?
|
||||
}
|
|
@ -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.matrix.android.api.session.call
|
||||
|
||||
enum class CallState {
|
||||
|
||||
/** Idle, setting up objects */
|
||||
IDLE,
|
||||
|
||||
/** Dialing. Outgoing call is signaling the remote peer */
|
||||
DIALING,
|
||||
|
||||
/** Answering. Incoming call is responding to remote peer */
|
||||
ANSWERING,
|
||||
|
||||
/** Remote ringing. Outgoing call, ICE negotiation is complete */
|
||||
REMOTE_RINGING,
|
||||
|
||||
/** Local ringing. Incoming call, ICE negotiation is complete */
|
||||
LOCAL_RINGING,
|
||||
|
||||
/** Connected. Incoming/Outgoing call, the call is connected */
|
||||
CONNECTED,
|
||||
|
||||
/** Terminated. Incoming/Outgoing call, the call is terminated */
|
||||
TERMINATED,
|
||||
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
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.CallCandidatesContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallHangupContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
|
||||
|
@ -43,6 +44,8 @@ interface CallsListener {
|
|||
|
||||
fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent)
|
||||
|
||||
fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent)
|
||||
|
||||
fun onCallAnswerReceived(callAnswerContent: CallAnswerContent)
|
||||
|
||||
fun onCallHangupReceived(callHangupContent: CallHangupContent)
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.webrtc.IceCandidate
|
|||
import org.webrtc.SessionDescription
|
||||
|
||||
interface MxCallDetail {
|
||||
val callId: String
|
||||
val isOutgoing: Boolean
|
||||
val roomId: String
|
||||
val otherUserId: String
|
||||
|
@ -30,6 +31,8 @@ interface MxCallDetail {
|
|||
* Define both an incoming call and on outgoing call
|
||||
*/
|
||||
interface MxCall : MxCallDetail {
|
||||
|
||||
var state: CallState
|
||||
/**
|
||||
* Pick Up the incoming call
|
||||
* It has no effect on outgoing call
|
||||
|
@ -62,4 +65,11 @@ interface MxCall : MxCallDetail {
|
|||
* Send removed ICE candidates to the other participant.
|
||||
*/
|
||||
fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>)
|
||||
|
||||
fun addListener(listener: StateListener)
|
||||
fun removeListener(listener: StateListener)
|
||||
|
||||
interface StateListener {
|
||||
fun onStateUpdate(call: MxCall)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
// /*
|
||||
// * 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>)
|
||||
// }
|
||||
// }
|
|
@ -48,7 +48,7 @@ data class CallCandidatesContent(
|
|||
/**
|
||||
* Required. The index of the SDP 'm' line this candidate is intended for.
|
||||
*/
|
||||
@Json(name = "sdpMLineIndex") val sdpMLineIndex: String,
|
||||
@Json(name = "sdpMLineIndex") val sdpMLineIndex: Int,
|
||||
/**
|
||||
* Required. The SDP 'a' line of the candidate.
|
||||
*/
|
||||
|
|
|
@ -28,7 +28,7 @@ import im.vector.matrix.android.api.session.Session
|
|||
import im.vector.matrix.android.api.session.account.AccountService
|
||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||
import im.vector.matrix.android.api.session.cache.CacheService
|
||||
import im.vector.matrix.android.api.session.call.CallService
|
||||
import im.vector.matrix.android.api.session.call.CallSignalingService
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
|
@ -112,7 +112,7 @@ internal class DefaultSession @Inject constructor(
|
|||
private val taskExecutor: TaskExecutor,
|
||||
private val widgetDependenciesHolder: WidgetDependenciesHolder,
|
||||
private val shieldTrustUpdater: ShieldTrustUpdater,
|
||||
private val callService: Lazy<CallService>)
|
||||
private val callSignalingService: Lazy<CallSignalingService>)
|
||||
: Session,
|
||||
RoomService by roomService.get(),
|
||||
RoomDirectoryService by roomDirectoryService.get(),
|
||||
|
@ -247,7 +247,7 @@ internal class DefaultSession @Inject constructor(
|
|||
|
||||
override fun integrationManagerService() = integrationManagerService
|
||||
|
||||
override fun callService(): CallService = callService.get()
|
||||
override fun callService(): CallSignalingService = callSignalingService.get()
|
||||
|
||||
override fun addListener(listener: Session.Listener) {
|
||||
sessionListeners.addListener(listener)
|
||||
|
|
|
@ -33,7 +33,6 @@ import im.vector.matrix.android.api.crypto.MXCryptoConfig
|
|||
import im.vector.matrix.android.api.session.InitialSyncProgressService
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||
import im.vector.matrix.android.api.session.call.CallService
|
||||
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
|
||||
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
|
||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
||||
|
@ -57,9 +56,10 @@ import im.vector.matrix.android.internal.network.NetworkCallbackStrategy
|
|||
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
||||
import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy
|
||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
|
||||
import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor
|
||||
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
||||
import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider
|
||||
import im.vector.matrix.android.internal.session.call.CallEventObserver
|
||||
import im.vector.matrix.android.internal.session.call.DefaultCallService
|
||||
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService
|
||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||
|
|
|
@ -39,7 +39,7 @@ internal interface CallEventsObserverTask : Task<CallEventsObserverTask.Params,
|
|||
internal class DefaultCallEventsObserverTask @Inject constructor(
|
||||
private val monarchy: Monarchy,
|
||||
private val cryptoService: CryptoService,
|
||||
private val callService: DefaultCallService) : CallEventsObserverTask {
|
||||
private val callService: DefaultCallSignalingService) : CallEventsObserverTask {
|
||||
|
||||
override suspend fun execute(params: CallEventsObserverTask.Params) {
|
||||
val events = params.events
|
||||
|
|
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.internal.session.call
|
|||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import im.vector.matrix.android.api.session.call.CallService
|
||||
import im.vector.matrix.android.api.session.call.CallSignalingService
|
||||
import im.vector.matrix.android.api.session.call.VoipApi
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import retrofit2.Retrofit
|
||||
|
@ -39,7 +39,7 @@ internal abstract class CallModule {
|
|||
|
||||
|
||||
@Binds
|
||||
abstract fun bindCallService(service:DefaultCallService): CallService
|
||||
abstract fun bindCallService(service:DefaultCallSignalingService): CallSignalingService
|
||||
|
||||
@Binds
|
||||
abstract fun bindTurnServerTask(task: DefaultGetTurnServerTask): GetTurnServerTask
|
||||
|
|
|
@ -18,7 +18,7 @@ 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.CallSignalingService
|
||||
import im.vector.matrix.android.api.session.call.CallsListener
|
||||
import im.vector.matrix.android.api.session.call.MxCall
|
||||
import im.vector.matrix.android.api.session.call.TurnServer
|
||||
|
@ -26,6 +26,7 @@ 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.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.CallHangupContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
@ -40,17 +41,19 @@ import java.util.UUID
|
|||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class DefaultCallService @Inject constructor(
|
||||
internal class DefaultCallSignalingService @Inject constructor(
|
||||
@UserId
|
||||
private val userId: String,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val roomEventSender: RoomEventSender,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val turnServerTask: GetTurnServerTask
|
||||
) : CallService {
|
||||
) : CallSignalingService {
|
||||
|
||||
private val callListeners = mutableSetOf<CallsListener>()
|
||||
|
||||
private val activeCalls = mutableListOf<MxCall>()
|
||||
|
||||
override fun getTurnServer(callback: MatrixCallback<TurnServer>): Cancelable {
|
||||
return turnServerTask
|
||||
.configureWith(GetTurnServerTask.Params) {
|
||||
|
@ -77,7 +80,9 @@ internal class DefaultCallService @Inject constructor(
|
|||
isVideoCall = isVideoCall,
|
||||
localEchoEventFactory = localEchoEventFactory,
|
||||
roomEventSender = roomEventSender
|
||||
)
|
||||
).also {
|
||||
activeCalls.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addCallListener(listener: CallsListener) {
|
||||
|
@ -88,14 +93,24 @@ internal class DefaultCallService @Inject constructor(
|
|||
callListeners.remove(listener)
|
||||
}
|
||||
|
||||
override fun getCallWithId(callId: String): MxCall? {
|
||||
return activeCalls.find { it.callId == callId }
|
||||
}
|
||||
|
||||
internal fun onCallEvent(event: Event) {
|
||||
// TODO if handled by other of my sessions
|
||||
// this test is too simple, should notify upstream
|
||||
if (event.senderId == userId) {
|
||||
//ignore local echos!
|
||||
return
|
||||
}
|
||||
when (event.getClearType()) {
|
||||
EventType.CALL_ANSWER -> {
|
||||
EventType.CALL_ANSWER -> {
|
||||
event.getClearContent().toModel<CallAnswerContent>()?.let {
|
||||
onCallAnswer(it)
|
||||
}
|
||||
}
|
||||
EventType.CALL_INVITE -> {
|
||||
EventType.CALL_INVITE -> {
|
||||
event.getClearContent().toModel<CallInviteContent>()?.let { content ->
|
||||
val incomingCall = MxCallImpl(
|
||||
callId = content.callId ?: return@let,
|
||||
|
@ -107,14 +122,24 @@ internal class DefaultCallService @Inject constructor(
|
|||
localEchoEventFactory = localEchoEventFactory,
|
||||
roomEventSender = roomEventSender
|
||||
)
|
||||
activeCalls.add(incomingCall)
|
||||
onCallInvite(incomingCall, content)
|
||||
}
|
||||
}
|
||||
EventType.CALL_HANGUP -> {
|
||||
event.getClearContent().toModel<CallHangupContent>()?.let {
|
||||
onCallHangup(it)
|
||||
EventType.CALL_HANGUP -> {
|
||||
event.getClearContent().toModel<CallHangupContent>()?.let { content ->
|
||||
onCallHangup(content)
|
||||
activeCalls.removeAll { it.callId == content.callId }
|
||||
}
|
||||
}
|
||||
EventType.CALL_CANDIDATES -> {
|
||||
event.getClearContent().toModel<CallCandidatesContent>()?.let { content ->
|
||||
activeCalls.firstOrNull { it.callId == content.callId }?.let {
|
||||
onCallIceCandidate(it, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,6 +170,14 @@ internal class DefaultCallService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onCallIceCandidate(incomingCall: MxCall, candidates: CallCandidatesContent) {
|
||||
callListeners.toList().forEach {
|
||||
tryThis {
|
||||
it.onCallIceCandidateReceived(incomingCall, candidates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CALL_TIMEOUT_MS = 120_000
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.call.model
|
||||
|
||||
import im.vector.matrix.android.api.session.call.CallState
|
||||
import im.vector.matrix.android.api.session.call.MxCall
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
|
@ -27,14 +28,15 @@ 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.CallHangupContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
import im.vector.matrix.android.internal.session.call.DefaultCallService
|
||||
import im.vector.matrix.android.internal.session.call.DefaultCallSignalingService
|
||||
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 timber.log.Timber
|
||||
|
||||
internal class MxCallImpl(
|
||||
val callId: String,
|
||||
override val callId: String,
|
||||
override val isOutgoing: Boolean,
|
||||
override val roomId: String,
|
||||
private val userId: String,
|
||||
|
@ -44,12 +46,47 @@ internal class MxCallImpl(
|
|||
private val roomEventSender: RoomEventSender
|
||||
) : MxCall {
|
||||
|
||||
override var state: CallState = CallState.IDLE
|
||||
set(value) {
|
||||
field = value
|
||||
dispatchStateChange()
|
||||
}
|
||||
|
||||
private val listeners = mutableListOf<MxCall.StateListener>()
|
||||
|
||||
override fun addListener(listener: MxCall.StateListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun removeListener(listener: MxCall.StateListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
private fun dispatchStateChange() {
|
||||
listeners.forEach {
|
||||
try {
|
||||
it.onStateUpdate(this)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.d("dispatchStateChange failed for call $callId : ${failure.localizedMessage}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
if (isOutgoing) {
|
||||
state = CallState.DIALING
|
||||
} else {
|
||||
state = CallState.LOCAL_RINGING
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun offerSdp(sdp: SessionDescription) {
|
||||
if (!isOutgoing) return
|
||||
|
||||
state = CallState.REMOTE_RINGING
|
||||
CallInviteContent(
|
||||
callId = callId,
|
||||
lifetime = DefaultCallService.CALL_TIMEOUT_MS,
|
||||
lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS,
|
||||
offer = CallInviteContent.Offer(sdp = sdp.description)
|
||||
)
|
||||
.let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) }
|
||||
|
@ -62,7 +99,7 @@ internal class MxCallImpl(
|
|||
candidates = candidates.map {
|
||||
CallCandidatesContent.Candidate(
|
||||
sdpMid = it.sdpMid,
|
||||
sdpMLineIndex = it.sdpMLineIndex.toString(),
|
||||
sdpMLineIndex = it.sdpMLineIndex,
|
||||
candidate = it.sdp
|
||||
)
|
||||
}
|
||||
|
@ -80,11 +117,12 @@ internal class MxCallImpl(
|
|||
)
|
||||
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
|
||||
.also { roomEventSender.sendEvent(it) }
|
||||
state = CallState.TERMINATED
|
||||
}
|
||||
|
||||
override fun accept(sdp: SessionDescription) {
|
||||
if (isOutgoing) return
|
||||
|
||||
state = CallState.ANSWERING
|
||||
CallAnswerContent(
|
||||
callId = callId,
|
||||
answer = CallAnswerContent.Answer(sdp = sdp.description)
|
||||
|
|
|
@ -363,6 +363,6 @@
|
|||
<string name="key_verification_request_fallback_message">%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys.</string>
|
||||
|
||||
<string name="call_notification_answer">Accept</string>
|
||||
<string name="call_notification_reject">Reject</string>
|
||||
<string name="call_notification_reject">Decline</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<!-- Needed for incoming calls -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
|
||||
|
@ -206,9 +207,9 @@
|
|||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".features.call.service.CallHeadsUpService"
|
||||
android:exported="false" />
|
||||
<!-- <service-->
|
||||
<!-- android:name=".features.call.service.CallHeadsUpService"-->
|
||||
<!-- android:exported="false" />-->
|
||||
|
||||
<!-- Receivers -->
|
||||
|
||||
|
|
|
@ -15,15 +15,16 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:Suppress("UNUSED_PARAMETER")
|
||||
|
||||
package im.vector.riotx.core.services
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.text.TextUtils
|
||||
import androidx.core.content.ContextCompat
|
||||
import im.vector.riotx.core.extensions.vectorComponent
|
||||
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.riotx.features.call.telecom.CallConnection
|
||||
import im.vector.riotx.features.notifications.NotificationUtils
|
||||
import timber.log.Timber
|
||||
|
@ -41,6 +42,7 @@ class CallService : VectorService() {
|
|||
private var mCallIdInProgress: String? = null
|
||||
|
||||
private lateinit var notificationUtils: NotificationUtils
|
||||
private lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||
|
||||
/**
|
||||
* incoming (foreground notification)
|
||||
|
@ -50,6 +52,7 @@ class CallService : VectorService() {
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notificationUtils = vectorComponent().notificationUtils()
|
||||
webRtcPeerConnectionManager = vectorComponent().webRtcPeerConnectionManager()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
@ -87,46 +90,46 @@ class CallService : VectorService() {
|
|||
private fun displayIncomingCallNotification(intent: Intent) {
|
||||
Timber.v("displayIncomingCallNotification")
|
||||
|
||||
// TODO
|
||||
/*
|
||||
|
||||
|
||||
// the incoming call in progress is already displayed
|
||||
if (!TextUtils.isEmpty(mIncomingCallId)) {
|
||||
Timber.v("displayIncomingCallNotification : the incoming call in progress is already displayed")
|
||||
} else if (!TextUtils.isEmpty(mCallIdInProgress)) {
|
||||
Timber.v("displayIncomingCallNotification : a 'call in progress' notification is displayed")
|
||||
} else if (null == CallsManager.getSharedInstance().activeCall) {
|
||||
} else
|
||||
// if (null == webRtcPeerConnectionManager.currentCall)
|
||||
{
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID)
|
||||
|
||||
Timber.v("displayIncomingCallNotification : display the dedicated notification")
|
||||
val notification = NotificationUtils.buildIncomingCallNotification(
|
||||
this,
|
||||
val notification = notificationUtils.buildIncomingCallNotification(
|
||||
intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
|
||||
intent.getStringExtra(EXTRA_ROOM_NAME),
|
||||
intent.getStringExtra(EXTRA_MATRIX_ID),
|
||||
callId)
|
||||
intent.getStringExtra(EXTRA_ROOM_NAME) ?: "",
|
||||
intent.getStringExtra(EXTRA_MATRIX_ID) ?: "",
|
||||
callId ?: "")
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
|
||||
mIncomingCallId = callId
|
||||
|
||||
// turn the screen on for 3 seconds
|
||||
if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) {
|
||||
try {
|
||||
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
val wl = pm.newWakeLock(
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
||||
CallService::class.java.simpleName)
|
||||
wl.acquire(3000)
|
||||
wl.release()
|
||||
} catch (re: RuntimeException) {
|
||||
Timber.e(re, "displayIncomingCallNotification : failed to turn screen on ")
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
Timber.i("displayIncomingCallNotification : do not display the incoming call notification because there is a pending call")
|
||||
}// test if there is no active call
|
||||
*/
|
||||
// if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) {
|
||||
// try {
|
||||
// val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
// val wl = pm.newWakeLock(
|
||||
// WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
||||
// CallService::class.java.simpleName)
|
||||
// wl.acquire(3000)
|
||||
// wl.release()
|
||||
// } catch (re: RuntimeException) {
|
||||
// Timber.e(re, "displayIncomingCallNotification : failed to turn screen on ")
|
||||
// }
|
||||
//
|
||||
// }
|
||||
}
|
||||
// else {
|
||||
// Timber.i("displayIncomingCallNotification : do not display the incoming call notification because there is a pending call")
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -169,6 +172,7 @@ class CallService : VectorService() {
|
|||
private const val ACTION_INCOMING_CALL = "im.vector.riotx.core.services.CallService.INCOMING_CALL"
|
||||
private const val ACTION_PENDING_CALL = "im.vector.riotx.core.services.CallService.PENDING_CALL"
|
||||
private const val ACTION_NO_ACTIVE_CALL = "im.vector.riotx.core.services.CallService.NO_ACTIVE_CALL"
|
||||
// private const val ACTION_ON_ACTIVE_CALL = "im.vector.riotx.core.services.CallService.ACTIVE_CALL"
|
||||
|
||||
private const val EXTRA_IS_VIDEO = "EXTRA_IS_VIDEO"
|
||||
private const val EXTRA_ROOM_NAME = "EXTRA_ROOM_NAME"
|
||||
|
@ -195,6 +199,25 @@ class CallService : VectorService() {
|
|||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
||||
// fun onActiveCall(context: Context,
|
||||
// isVideo: Boolean,
|
||||
// roomName: String,
|
||||
// roomId: String,
|
||||
// matrixId: String,
|
||||
// callId: String) {
|
||||
// val intent = Intent(context, CallService::class.java)
|
||||
// .apply {
|
||||
// action = ACTION_ON_ACTIVE_CALL
|
||||
// putExtra(EXTRA_IS_VIDEO, isVideo)
|
||||
// putExtra(EXTRA_ROOM_NAME, roomName)
|
||||
// putExtra(EXTRA_ROOM_ID, roomId)
|
||||
// putExtra(EXTRA_MATRIX_ID, matrixId)
|
||||
// putExtra(EXTRA_CALL_ID, callId)
|
||||
// }
|
||||
//
|
||||
// ContextCompat.startForegroundService(context, intent)
|
||||
// }
|
||||
|
||||
fun onPendingCall(context: Context,
|
||||
isVideo: Boolean,
|
||||
roomName: String,
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isVisible
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import butterknife.OnClick
|
||||
import im.vector.matrix.android.api.session.call.CallState
|
||||
import im.vector.riotx.R
|
||||
|
||||
class CallControlsView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
@BindView(R.id.incomingRingingControls)
|
||||
lateinit var incomingRingingControls: ViewGroup
|
||||
// @BindView(R.id.iv_icr_accept_call)
|
||||
// lateinit var incomingRingingControlAccept: ImageView
|
||||
// @BindView(R.id.iv_icr_end_call)
|
||||
// lateinit var incomingRingingControlDecline: ImageView
|
||||
|
||||
@BindView(R.id.connectedControls)
|
||||
lateinit var connectedControls: ViewGroup
|
||||
|
||||
init {
|
||||
ConstraintLayout.inflate(context, R.layout.fragment_call_controls, this)
|
||||
//layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
ButterKnife.bind(this)
|
||||
}
|
||||
|
||||
@OnClick(R.id.iv_icr_accept_call)
|
||||
fun acceptIncomingCall() {
|
||||
interactionListener?.didAcceptIncomingCall()
|
||||
}
|
||||
|
||||
@OnClick(R.id.iv_icr_end_call)
|
||||
fun declineIncomingCall() {
|
||||
interactionListener?.didDeclineIncomingCall()
|
||||
}
|
||||
|
||||
@OnClick(R.id.iv_end_call)
|
||||
fun endOngoingCall() {
|
||||
interactionListener?.didEndCall()
|
||||
}
|
||||
|
||||
@OnClick(R.id.iv_end_call)
|
||||
fun hangupCall() {
|
||||
}
|
||||
|
||||
fun updateForState(callState: CallState?) {
|
||||
when (callState) {
|
||||
CallState.DIALING -> {
|
||||
}
|
||||
CallState.ANSWERING -> {
|
||||
incomingRingingControls.isVisible = false
|
||||
connectedControls.isVisible = false
|
||||
}
|
||||
CallState.REMOTE_RINGING -> {
|
||||
}
|
||||
CallState.LOCAL_RINGING -> {
|
||||
incomingRingingControls.isVisible = true
|
||||
connectedControls.isVisible = false
|
||||
}
|
||||
CallState.CONNECTED -> {
|
||||
incomingRingingControls.isVisible = false
|
||||
connectedControls.isVisible = true
|
||||
}
|
||||
CallState.TERMINATED,
|
||||
CallState.IDLE,
|
||||
null -> {
|
||||
incomingRingingControls.isVisible = false
|
||||
connectedControls.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface InteractionListener {
|
||||
fun didAcceptIncomingCall()
|
||||
fun didDeclineIncomingCall()
|
||||
fun didEndCall()
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ import org.webrtc.SdpObserver
|
|||
import org.webrtc.SessionDescription
|
||||
import timber.log.Timber
|
||||
|
||||
abstract class SdpObserverAdapter : SdpObserver {
|
||||
open class SdpObserverAdapter : SdpObserver {
|
||||
override fun onSetFailure(p0: String?) {
|
||||
Timber.e("## SdpObserver: onSetFailure $p0")
|
||||
}
|
||||
|
|
|
@ -16,52 +16,58 @@
|
|||
|
||||
package im.vector.riotx.features.call
|
||||
|
||||
//import im.vector.riotx.features.call.service.CallHeadsUpService
|
||||
import android.app.KeyguardManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import butterknife.BindView
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.viewModel
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
import im.vector.matrix.android.api.session.call.CallState
|
||||
import im.vector.matrix.android.api.session.call.EglUtils
|
||||
import im.vector.matrix.android.api.session.call.MxCallDetail
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.core.services.CallService
|
||||
import im.vector.riotx.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
|
||||
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 im.vector.riotx.features.call.service.CallHeadsUpService
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.activity_call.*
|
||||
import org.webrtc.EglBase
|
||||
import org.webrtc.RendererCommon
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
data class CallArgs(
|
||||
val roomId: String,
|
||||
val callId: String?,
|
||||
val participantUserId: String,
|
||||
val isIncomingCall: Boolean,
|
||||
val isVideoCall: Boolean
|
||||
val isVideoCall: Boolean,
|
||||
val autoAccept: Boolean
|
||||
) : Parcelable
|
||||
|
||||
class VectorCallActivity : VectorBaseActivity() {
|
||||
class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionListener {
|
||||
|
||||
override fun getLayoutRes() = R.layout.activity_call
|
||||
|
||||
@Inject lateinit var avatarRenderer: AvatarRenderer
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
super.injectWith(injector)
|
||||
injector.inject(this)
|
||||
|
@ -80,18 +86,21 @@ class VectorCallActivity : VectorBaseActivity() {
|
|||
@BindView(R.id.fullscreen_video_view)
|
||||
lateinit var fullscreenRenderer: SurfaceViewRenderer
|
||||
|
||||
@BindView(R.id.callControls)
|
||||
lateinit var callControlsView: CallControlsView
|
||||
|
||||
private var rootEglBase: EglBase? = null
|
||||
|
||||
var callHeadsUpService: CallHeadsUpService? = null
|
||||
private val serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
callHeadsUpService = (service as? CallHeadsUpService.CallHeadsUpServiceBinder)?.getService()
|
||||
}
|
||||
}
|
||||
// var callHeadsUpService: CallHeadsUpService? = null
|
||||
// private val serviceConnection = object : ServiceConnection {
|
||||
// override fun onServiceDisconnected(name: ComponentName?) {
|
||||
// finish()
|
||||
// }
|
||||
//
|
||||
// override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
// callHeadsUpService = (service as? CallHeadsUpService.CallHeadsUpServiceBinder)?.getService()
|
||||
// }
|
||||
// }
|
||||
|
||||
override fun doBeforeSetContentView() {
|
||||
// Set window styles for fullscreen-window size. Needs to be done before adding content.
|
||||
|
@ -118,8 +127,11 @@ class VectorCallActivity : VectorBaseActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
tryThis { unbindService(serviceConnection) }
|
||||
bindService(Intent(this, CallHeadsUpService::class.java), serviceConnection, 0)
|
||||
// window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
||||
// window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
|
||||
// tryThis { unbindService(serviceConnection) }
|
||||
// bindService(Intent(this, CallHeadsUpService::class.java), serviceConnection, 0)
|
||||
|
||||
if (intent.hasExtra(MvRx.KEY_ARG)) {
|
||||
callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!!
|
||||
|
@ -127,12 +139,29 @@ class VectorCallActivity : VectorBaseActivity() {
|
|||
finish()
|
||||
}
|
||||
|
||||
if (isFirstCreation()) {
|
||||
// Reduce priority of notification as the activity is on screen
|
||||
CallService.onPendingCall(
|
||||
this,
|
||||
callArgs.isVideoCall,
|
||||
callArgs.participantUserId,
|
||||
callArgs.roomId,
|
||||
"",
|
||||
callArgs.callId ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
rootEglBase = EglUtils.rootEglBase ?: return Unit.also {
|
||||
finish()
|
||||
}
|
||||
|
||||
configureCallViews()
|
||||
|
||||
callViewModel.subscribe(this) {
|
||||
renderState(it)
|
||||
}
|
||||
|
||||
|
||||
callViewModel.viewEvents
|
||||
.observe()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
@ -141,26 +170,89 @@ class VectorCallActivity : VectorBaseActivity() {
|
|||
}
|
||||
.disposeOnDestroy()
|
||||
|
||||
if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, this, CAPTURE_PERMISSION_REQUEST_CODE, R.string.permissions_rationale_msg_camera_and_audio)) {
|
||||
start()
|
||||
if (callArgs.isVideoCall) {
|
||||
if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, this, CAPTURE_PERMISSION_REQUEST_CODE, R.string.permissions_rationale_msg_camera_and_audio)) {
|
||||
start()
|
||||
}
|
||||
} else {
|
||||
if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, this, CAPTURE_PERMISSION_REQUEST_CODE, R.string.permissions_rationale_msg_record_audio)) {
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderState(state: VectorCallViewState) {
|
||||
Timber.v("## VOIP renderState call $state")
|
||||
callControlsView.updateForState(state.callState.invoke())
|
||||
when (state.callState.invoke()) {
|
||||
CallState.IDLE -> {
|
||||
|
||||
}
|
||||
CallState.DIALING -> {
|
||||
callVideoGroup.isInvisible = true
|
||||
callInfoGroup.isVisible = true
|
||||
callStatusText.setText(R.string.call_ring)
|
||||
state.otherUserMatrixItem.invoke()?.let {
|
||||
avatarRenderer.render(it, otherMemberAvatar)
|
||||
participantNameText.text = it.getBestName()
|
||||
callTypeText.setText(if (state.isVideoCall) R.string.action_video_call else R.string.action_voice_call)
|
||||
}
|
||||
}
|
||||
CallState.ANSWERING -> {
|
||||
callInfoGroup.isVisible = true
|
||||
callStatusText.setText(R.string.call_connecting)
|
||||
state.otherUserMatrixItem.invoke()?.let {
|
||||
avatarRenderer.render(it, otherMemberAvatar)
|
||||
}
|
||||
// fullscreenRenderer.isVisible = true
|
||||
// pipRenderer.isVisible = true
|
||||
}
|
||||
CallState.REMOTE_RINGING -> {
|
||||
callVideoGroup.isInvisible = true
|
||||
callInfoGroup.isVisible = true
|
||||
callStatusText.setText(
|
||||
if (state.isVideoCall) R.string.incoming_video_call else R.string.incoming_voice_call
|
||||
)
|
||||
}
|
||||
CallState.LOCAL_RINGING -> {
|
||||
callVideoGroup.isInvisible = true
|
||||
callInfoGroup.isVisible = true
|
||||
state.otherUserMatrixItem.invoke()?.let {
|
||||
avatarRenderer.render(it, otherMemberAvatar)
|
||||
participantNameText.text = it.getBestName()
|
||||
callTypeText.setText(if (state.isVideoCall) R.string.action_video_call else R.string.action_voice_call)
|
||||
}
|
||||
}
|
||||
CallState.CONNECTED -> {
|
||||
// TODO only if is video call
|
||||
callVideoGroup.isVisible = true
|
||||
callInfoGroup.isVisible = false
|
||||
}
|
||||
CallState.TERMINATED -> {
|
||||
finish()
|
||||
}
|
||||
null -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureCallViews() {
|
||||
if (callArgs.isVideoCall) {
|
||||
iv_call_speaker.isVisible = false
|
||||
iv_call_flip_camera.isVisible = true
|
||||
iv_call_videocam_off.isVisible = true
|
||||
} else {
|
||||
iv_call_speaker.isVisible = true
|
||||
iv_call_flip_camera.isVisible = false
|
||||
iv_call_videocam_off.isVisible = false
|
||||
}
|
||||
|
||||
iv_end_call.setOnClickListener {
|
||||
callViewModel.handle(VectorCallViewActions.EndCall)
|
||||
finish()
|
||||
}
|
||||
callControlsView.interactionListener = this
|
||||
// if (callArgs.isVideoCall) {
|
||||
// iv_call_speaker.isVisible = false
|
||||
// iv_call_flip_camera.isVisible = true
|
||||
// iv_call_videocam_off.isVisible = true
|
||||
// } else {
|
||||
// iv_call_speaker.isVisible = true
|
||||
// iv_call_flip_camera.isVisible = false
|
||||
// iv_call_videocam_off.isVisible = false
|
||||
// }
|
||||
//
|
||||
// iv_end_call.setOnClickListener {
|
||||
// callViewModel.handle(VectorCallViewActions.EndCall)
|
||||
// finish()
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
|
@ -186,13 +278,14 @@ class VectorCallActivity : VectorBaseActivity() {
|
|||
fullscreenRenderer.setEnableHardwareScaler(true /* enabled */)
|
||||
|
||||
|
||||
peerConnectionManager.attachViewRenderers(pipRenderer, fullscreenRenderer)
|
||||
peerConnectionManager.attachViewRenderers(pipRenderer, fullscreenRenderer,
|
||||
intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() })
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
peerConnectionManager.detachRenderers()
|
||||
tryThis { unbindService(serviceConnection) }
|
||||
// tryThis { unbindService(serviceConnection) }
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
@ -209,12 +302,45 @@ class VectorCallActivity : VectorBaseActivity() {
|
|||
companion object {
|
||||
|
||||
private const val CAPTURE_PERMISSION_REQUEST_CODE = 1
|
||||
private const val EXTRA_MODE = "EXTRA_MODE"
|
||||
|
||||
const val OUTGOING_CREATED = "OUTGOING_CREATED"
|
||||
const val INCOMING_RINGING = "INCOMING_RINGING"
|
||||
const val INCOMING_ACCEPT = "INCOMING_ACCEPT"
|
||||
|
||||
fun newIntent(context: Context, mxCall: MxCallDetail): Intent {
|
||||
return Intent(context, VectorCallActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
putExtra(MvRx.KEY_ARG, CallArgs(mxCall.roomId, mxCall.otherUserId, !mxCall.isOutgoing, mxCall.isVideoCall))
|
||||
putExtra(MvRx.KEY_ARG, CallArgs(mxCall.roomId, mxCall.callId, mxCall.otherUserId, !mxCall.isOutgoing, mxCall.isVideoCall, false))
|
||||
putExtra(EXTRA_MODE, OUTGOING_CREATED)
|
||||
}
|
||||
}
|
||||
|
||||
fun newIntent(context: Context,
|
||||
callId: String?,
|
||||
roomId: String,
|
||||
otherUserId: String,
|
||||
isIncomingCall: Boolean,
|
||||
isVideoCall: Boolean,
|
||||
accept: Boolean,
|
||||
mode: String?): Intent {
|
||||
return Intent(context, VectorCallActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
putExtra(MvRx.KEY_ARG, CallArgs(roomId, callId, otherUserId, isIncomingCall, isVideoCall, accept))
|
||||
putExtra(EXTRA_MODE, mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun didAcceptIncomingCall() {
|
||||
callViewModel.handle(VectorCallViewActions.AcceptCall)
|
||||
}
|
||||
|
||||
override fun didDeclineIncomingCall() {
|
||||
callViewModel.handle(VectorCallViewActions.DeclineCall)
|
||||
}
|
||||
|
||||
override fun didEndCall() {
|
||||
callViewModel.handle(VectorCallViewActions.EndCall)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,17 +16,22 @@
|
|||
|
||||
package im.vector.riotx.features.call
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
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.call.CallState
|
||||
import im.vector.matrix.android.api.session.call.MxCall
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallAnswerContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallHangupContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.platform.VectorViewEvents
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
|
@ -34,65 +39,117 @@ import im.vector.riotx.core.platform.VectorViewModelAction
|
|||
|
||||
data class VectorCallViewState(
|
||||
val callId: String? = null,
|
||||
val roomId: String = ""
|
||||
val roomId: String = "",
|
||||
val isVideoCall: Boolean,
|
||||
val otherUserMatrixItem: Async<MatrixItem> = Uninitialized,
|
||||
val callState: Async<CallState> = Uninitialized
|
||||
) : MvRxState
|
||||
|
||||
sealed class VectorCallViewActions : VectorViewModelAction {
|
||||
|
||||
object EndCall : VectorCallViewActions()
|
||||
object AcceptCall : VectorCallViewActions()
|
||||
object DeclineCall : VectorCallViewActions()
|
||||
}
|
||||
|
||||
sealed class VectorCallViewEvents : VectorViewEvents {
|
||||
|
||||
data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
|
||||
data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
|
||||
object CallAccepted : VectorCallViewEvents()
|
||||
}
|
||||
|
||||
class VectorCallViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: VectorCallViewState,
|
||||
@Assisted val args: CallArgs,
|
||||
val session: Session,
|
||||
val webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||
) : 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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(mxCall: MxCall, callInviteContent: CallInviteContent) {
|
||||
// }
|
||||
//
|
||||
// override fun onCallHangupReceived(callHangupContent: CallHangupContent) {
|
||||
// withState { state ->
|
||||
// if (callHangupContent.callId == state.callId) {
|
||||
// _viewEvents.post(VectorCallViewEvents.CallHangup(callHangupContent))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
var autoReplyIfNeeded: Boolean = false
|
||||
|
||||
override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) {
|
||||
}
|
||||
var call: MxCall? = null
|
||||
|
||||
override fun onCallHangupReceived(callHangupContent: CallHangupContent) {
|
||||
withState { state ->
|
||||
if (callHangupContent.callId == state.callId) {
|
||||
_viewEvents.post(VectorCallViewEvents.CallHangup(callHangupContent))
|
||||
}
|
||||
private val callStateListener = object : MxCall.StateListener {
|
||||
override fun onStateUpdate(call: MxCall) {
|
||||
setState {
|
||||
copy(
|
||||
callState = Success(call.state)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
session.callService().addCallListener(callServiceListener)
|
||||
|
||||
autoReplyIfNeeded = args.autoAccept
|
||||
|
||||
initialState.callId?.let {
|
||||
session.callService().getCallWithId(it)?.let { mxCall ->
|
||||
this.call = mxCall
|
||||
mxCall.otherUserId
|
||||
val item: MatrixItem? = session.getUser(mxCall.otherUserId)?.toMatrixItem()
|
||||
|
||||
mxCall.addListener(callStateListener)
|
||||
setState {
|
||||
copy(
|
||||
isVideoCall = mxCall.isVideoCall,
|
||||
callState = Success(mxCall.state),
|
||||
otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//session.callService().addCallListener(callServiceListener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
session.callService().removeCallListener(callServiceListener)
|
||||
//session.callService().removeCallListener(callServiceListener)
|
||||
this.call?.removeListener(callStateListener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
override fun handle(action: VectorCallViewActions) = withState {
|
||||
when (action) {
|
||||
VectorCallViewActions.EndCall -> webRtcPeerConnectionManager.endCall()
|
||||
VectorCallViewActions.EndCall -> webRtcPeerConnectionManager.endCall()
|
||||
VectorCallViewActions.AcceptCall -> {
|
||||
setState {
|
||||
copy(callState = Loading())
|
||||
}
|
||||
webRtcPeerConnectionManager.acceptIncomingCall()
|
||||
}
|
||||
VectorCallViewActions.DeclineCall -> {
|
||||
setState {
|
||||
copy(callState = Loading())
|
||||
}
|
||||
webRtcPeerConnectionManager.endCall()
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: VectorCallViewState): VectorCallViewModel
|
||||
fun create(initialState: VectorCallViewState, args: CallArgs): VectorCallViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<VectorCallViewModel, VectorCallViewState> {
|
||||
|
@ -100,12 +157,17 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: VectorCallViewState): VectorCallViewModel? {
|
||||
val callActivity: VectorCallActivity = viewModelContext.activity()
|
||||
return callActivity.viewModelFactory.create(state)
|
||||
val callArgs: CallArgs = viewModelContext.args()
|
||||
return callActivity.viewModelFactory.create(state, callArgs)
|
||||
}
|
||||
|
||||
override fun initialState(viewModelContext: ViewModelContext): VectorCallViewState? {
|
||||
//val args: CallArgs = viewModelContext.args()
|
||||
return VectorCallViewState()
|
||||
val args: CallArgs = viewModelContext.args()
|
||||
return VectorCallViewState(
|
||||
callId = args.callId,
|
||||
roomId = args.roomId,
|
||||
isVideoCall = args.isVideoCall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,26 +16,22 @@
|
|||
|
||||
package im.vector.riotx.features.call
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import androidx.core.content.ContextCompat
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
import im.vector.matrix.android.api.session.call.CallState
|
||||
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.call.MxCall
|
||||
import im.vector.matrix.android.api.session.call.MxCallDetail
|
||||
import im.vector.matrix.android.api.session.call.TurnServer
|
||||
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.CallHangupContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.features.call.service.CallHeadsUpService
|
||||
import im.vector.riotx.core.services.CallService
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import io.reactivex.subjects.ReplaySubject
|
||||
import org.webrtc.AudioSource
|
||||
import org.webrtc.AudioTrack
|
||||
import org.webrtc.Camera1Enumerator
|
||||
|
@ -72,51 +68,86 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
|||
private val sessionHolder: ActiveSessionHolder
|
||||
) : CallsListener {
|
||||
|
||||
var localMediaStream: MediaStream? = null
|
||||
data class CallContext(
|
||||
val mxCall: MxCall,
|
||||
|
||||
var peerConnection: PeerConnection? = null,
|
||||
|
||||
var localMediaStream: MediaStream? = null,
|
||||
var remoteMediaStream: MediaStream? = null,
|
||||
|
||||
var localAudioSource: AudioSource? = null,
|
||||
var localAudioTrack: AudioTrack? = null,
|
||||
|
||||
var localVideoSource: VideoSource? = null,
|
||||
var localVideoTrack: VideoTrack? = null,
|
||||
|
||||
var remoteVideoTrack: VideoTrack? = null
|
||||
) {
|
||||
|
||||
var offerSdp: CallInviteContent.Offer? = null
|
||||
|
||||
val iceCandidateSource: PublishSubject<IceCandidate> = PublishSubject.create()
|
||||
private val iceCandidateDisposable = iceCandidateSource
|
||||
.buffer(300, TimeUnit.MILLISECONDS)
|
||||
.subscribe {
|
||||
// omit empty :/
|
||||
if (it.isNotEmpty()) {
|
||||
Timber.v("## Sending local ice candidates to call")
|
||||
//it.forEach { peerConnection?.addIceCandidate(it) }
|
||||
mxCall.sendLocalIceCandidates(it)
|
||||
}
|
||||
}
|
||||
|
||||
var remoteCandidateSource: ReplaySubject<IceCandidate>? = null
|
||||
var remoteIceCandidateDisposable: Disposable? = null
|
||||
|
||||
fun release() {
|
||||
|
||||
remoteIceCandidateDisposable?.dispose()
|
||||
iceCandidateDisposable?.dispose()
|
||||
|
||||
peerConnection?.close()
|
||||
peerConnection?.dispose()
|
||||
|
||||
localAudioSource?.dispose()
|
||||
localVideoSource?.dispose()
|
||||
|
||||
localAudioSource = null
|
||||
localAudioTrack = null
|
||||
localVideoSource = null
|
||||
localVideoTrack = null
|
||||
localMediaStream = null
|
||||
remoteMediaStream = null
|
||||
}
|
||||
}
|
||||
|
||||
// var localMediaStream: MediaStream? = null
|
||||
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
private val rootEglBase by lazy { EglUtils.rootEglBase }
|
||||
|
||||
private var peerConnectionFactory: PeerConnectionFactory? = null
|
||||
private var peerConnection: PeerConnection? = null
|
||||
|
||||
private var localSdp: SessionDescription? = null
|
||||
private var sdpObserver = SdpObserver()
|
||||
private var streamObserver = StreamObserver()
|
||||
|
||||
private var localViewRenderer: SurfaceViewRenderer? = null
|
||||
private var remoteViewRenderer: SurfaceViewRenderer? = null
|
||||
|
||||
private var remoteVideoTrack: VideoTrack? = null
|
||||
private var localVideoTrack: VideoTrack? = null
|
||||
|
||||
private var videoSource: VideoSource? = null
|
||||
private var audioSource: AudioSource? = null
|
||||
private var localAudioTrack: AudioTrack? = null
|
||||
// private var localSdp: SessionDescription? = null
|
||||
|
||||
private var videoCapturer: VideoCapturer? = null
|
||||
|
||||
var localSurfaceRenderer: WeakReference<SurfaceViewRenderer>? = null
|
||||
var remoteSurfaceRenderer: WeakReference<SurfaceViewRenderer>? = null
|
||||
|
||||
private val iceCandidateSource: PublishSubject<IceCandidate> = PublishSubject.create()
|
||||
private var iceCandidateDisposable: Disposable? = null
|
||||
var currentCall: CallContext? = null
|
||||
|
||||
var callHeadsUpService: CallHeadsUpService? = null
|
||||
|
||||
private var currentCall: MxCall? = null
|
||||
|
||||
private val serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
callHeadsUpService = (service as? CallHeadsUpService.CallHeadsUpServiceBinder)?.getService()
|
||||
init {
|
||||
// TODO do this lazyly
|
||||
executor.execute {
|
||||
createPeerConnectionFactory()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPeerConnectionFactory() {
|
||||
if (peerConnectionFactory != null) return
|
||||
Timber.v("## VOIP createPeerConnectionFactory")
|
||||
val eglBaseContext = rootEglBase?.eglBaseContext ?: return Unit.also {
|
||||
Timber.e("## VOIP No EGL BASE")
|
||||
|
@ -143,10 +174,10 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
|||
.setVideoDecoderFactory(defaultVideoDecoderFactory)
|
||||
.createPeerConnectionFactory()
|
||||
|
||||
attachViewRenderersInternal()
|
||||
//attachViewRenderersInternal()
|
||||
}
|
||||
|
||||
private fun createPeerConnection(turnServer: TurnServer?) {
|
||||
private fun createPeerConnection(callContext: CallContext, turnServer: TurnServer?) {
|
||||
val iceServers = mutableListOf<PeerConnection.IceServer>().apply {
|
||||
turnServer?.let { server ->
|
||||
server.uris?.forEach { uri ->
|
||||
|
@ -161,124 +192,228 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
Timber.v("## VOIP creating peer connection... ")
|
||||
peerConnection = peerConnectionFactory?.createPeerConnection(iceServers, streamObserver)
|
||||
Timber.v("## VOIP creating peer connection...with iceServers ${iceServers} ")
|
||||
callContext.peerConnection = peerConnectionFactory?.createPeerConnection(iceServers, StreamObserver(callContext))
|
||||
}
|
||||
|
||||
private fun startCall(turnServer: TurnServer?) {
|
||||
iceCandidateDisposable = iceCandidateSource
|
||||
.buffer(400, TimeUnit.MILLISECONDS)
|
||||
.subscribe {
|
||||
// omit empty :/
|
||||
if (it.isNotEmpty()) {
|
||||
Timber.v("## Sending local ice candidates to call")
|
||||
it.forEach { peerConnection?.addIceCandidate(it) }
|
||||
currentCall?.sendLocalIceCandidates(it)
|
||||
private fun sendSdpOffer(callContext: CallContext) {
|
||||
// executor.execute {
|
||||
val constraints = MediaConstraints()
|
||||
constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
|
||||
constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (currentCall?.mxCall?.isVideoCall == true) "true" else "false"))
|
||||
|
||||
Timber.v("## VOIP creating offer...")
|
||||
callContext.peerConnection?.createOffer(object : SdpObserverAdapter() {
|
||||
override fun onCreateSuccess(p0: SessionDescription?) {
|
||||
if (p0 == null) return
|
||||
// localSdp = p0
|
||||
callContext.peerConnection?.setLocalDescription(object : SdpObserverAdapter() {}, p0)
|
||||
// send offer to peer
|
||||
currentCall?.mxCall?.offerSdp(p0)
|
||||
}
|
||||
}, constraints)
|
||||
// }
|
||||
}
|
||||
|
||||
private fun getTurnServer(callback: ((TurnServer?) -> Unit)) {
|
||||
sessionHolder.getActiveSession().callService().getTurnServer(object : MatrixCallback<TurnServer?> {
|
||||
override fun onSuccess(data: TurnServer?) {
|
||||
callback(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
|
||||
Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer")
|
||||
this.localSurfaceRenderer = WeakReference(localViewRenderer)
|
||||
this.remoteSurfaceRenderer = WeakReference(remoteViewRenderer)
|
||||
getTurnServer { turnServer ->
|
||||
val call = currentCall ?: return@getTurnServer
|
||||
when (mode) {
|
||||
VectorCallActivity.INCOMING_ACCEPT -> {
|
||||
internalAcceptIncomingCall(call, turnServer)
|
||||
}
|
||||
VectorCallActivity.INCOMING_RINGING -> {
|
||||
// wait until accepted to create peer connection
|
||||
// TODO eventually we could already display local stream in PIP?
|
||||
}
|
||||
VectorCallActivity.OUTGOING_CREATED -> {
|
||||
executor.execute {
|
||||
// 1. Create RTCPeerConnection
|
||||
createPeerConnection(call, turnServer)
|
||||
|
||||
// 2. Access camera (if video call) + microphone, create local stream
|
||||
createLocalStream(call)
|
||||
|
||||
// 3. add local stream
|
||||
call.localMediaStream?.let { call.peerConnection?.addStream(it) }
|
||||
attachViewRenderersInternal()
|
||||
|
||||
// create an offer, set local description and send via signaling
|
||||
sendSdpOffer(call)
|
||||
|
||||
Timber.v("## VOIP remoteCandidateSource ${call.remoteCandidateSource}")
|
||||
call.remoteIceCandidateDisposable = call.remoteCandidateSource?.subscribe({
|
||||
Timber.v("## VOIP adding remote ice candidate $it")
|
||||
call.peerConnection?.addIceCandidate(it)
|
||||
}, {
|
||||
Timber.v("## VOIP failed to add remote ice candidate $it")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
executor.execute {
|
||||
createPeerConnectionFactory()
|
||||
createPeerConnection(turnServer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendSdpOffer() {
|
||||
executor.execute {
|
||||
val constraints = MediaConstraints()
|
||||
constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
|
||||
constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (currentCall?.isVideoCall == true) "true" else "false"))
|
||||
|
||||
Timber.v("## VOIP creating offer...")
|
||||
peerConnection?.createOffer(sdpObserver, constraints)
|
||||
}
|
||||
}
|
||||
|
||||
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer, remoteViewRenderer: SurfaceViewRenderer) {
|
||||
executor.execute {
|
||||
this.localViewRenderer = localViewRenderer
|
||||
this.remoteViewRenderer = remoteViewRenderer
|
||||
this.localSurfaceRenderer = WeakReference(localViewRenderer)
|
||||
this.remoteSurfaceRenderer = WeakReference(remoteViewRenderer)
|
||||
|
||||
if (peerConnection != null) {
|
||||
attachViewRenderersInternal()
|
||||
else -> {
|
||||
// sink existing tracks (configuration change, e.g screen rotation)
|
||||
attachViewRenderersInternal()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun internalAcceptIncomingCall(callContext: CallContext, turnServer: TurnServer?) {
|
||||
executor.execute {
|
||||
// 1) create peer connection
|
||||
createPeerConnection(callContext, turnServer)
|
||||
|
||||
// create sdp using offer, and set remote description
|
||||
// the offer has beed stored when invite was received
|
||||
callContext.offerSdp?.sdp?.let {
|
||||
SessionDescription(SessionDescription.Type.OFFER, it)
|
||||
}?.let {
|
||||
callContext.peerConnection?.setRemoteDescription(SdpObserverAdapter(), it)
|
||||
}
|
||||
// 2) Access camera + microphone, create local stream
|
||||
createLocalStream(callContext)
|
||||
|
||||
// 2) add local stream
|
||||
currentCall?.localMediaStream?.let { callContext.peerConnection?.addStream(it) }
|
||||
attachViewRenderersInternal()
|
||||
|
||||
// create a answer, set local description and send via signaling
|
||||
createAnswer()
|
||||
|
||||
Timber.v("## VOIP remoteCandidateSource ${callContext.remoteCandidateSource}")
|
||||
callContext.remoteIceCandidateDisposable = callContext.remoteCandidateSource?.subscribe({
|
||||
Timber.v("## VOIP adding remote ice candidate $it")
|
||||
callContext.peerConnection?.addIceCandidate(it)
|
||||
}, {
|
||||
Timber.v("## VOIP failed to add remote ice candidate $it")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLocalStream(callContext: CallContext) {
|
||||
|
||||
if (callContext.localMediaStream != null) {
|
||||
Timber.e("## VOIP localMediaStream already created")
|
||||
return
|
||||
}
|
||||
if (peerConnectionFactory == null) {
|
||||
Timber.e("## VOIP peerConnectionFactory is null")
|
||||
return
|
||||
}
|
||||
val audioSource = peerConnectionFactory!!.createAudioSource(DEFAULT_AUDIO_CONSTRAINTS)
|
||||
val localAudioTrack = peerConnectionFactory!!.createAudioTrack(AUDIO_TRACK_ID, audioSource)
|
||||
localAudioTrack?.setEnabled(true)
|
||||
|
||||
callContext.localAudioSource = audioSource
|
||||
callContext.localAudioTrack = localAudioTrack
|
||||
|
||||
val localMediaStream = peerConnectionFactory!!.createLocalMediaStream("ARDAMS") // magic value?
|
||||
|
||||
//Add audio track
|
||||
localMediaStream?.addTrack(localAudioTrack)
|
||||
|
||||
callContext.localMediaStream = localMediaStream
|
||||
|
||||
//add video track if needed
|
||||
if (callContext.mxCall.isVideoCall) {
|
||||
val cameraIterator = if (Camera2Enumerator.isSupported(context)) Camera2Enumerator(context) else Camera1Enumerator(false)
|
||||
val frontCamera = cameraIterator.deviceNames
|
||||
?.firstOrNull { cameraIterator.isFrontFacing(it) }
|
||||
?: cameraIterator.deviceNames?.first()
|
||||
|
||||
val videoCapturer = cameraIterator.createCapturer(frontCamera, null)
|
||||
|
||||
val videoSource = peerConnectionFactory!!.createVideoSource(videoCapturer.isScreencast)
|
||||
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext)
|
||||
Timber.v("## VOIP Local video source created")
|
||||
|
||||
videoCapturer.initialize(surfaceTextureHelper, context.applicationContext, videoSource!!.capturerObserver)
|
||||
// HD
|
||||
videoCapturer.startCapture(1280, 720, 30)
|
||||
|
||||
this.videoCapturer = videoCapturer
|
||||
|
||||
val localVideoTrack = peerConnectionFactory!!.createVideoTrack("ARDAMSv0", videoSource)
|
||||
Timber.v("## VOIP Local video track created")
|
||||
localVideoTrack?.setEnabled(true)
|
||||
|
||||
callContext.localVideoSource = videoSource
|
||||
callContext.localVideoTrack = localVideoTrack
|
||||
|
||||
// localViewRenderer?.let { localVideoTrack?.addSink(it) }
|
||||
localMediaStream?.addTrack(localVideoTrack)
|
||||
callContext.localMediaStream = localMediaStream
|
||||
// remoteVideoTrack?.setEnabled(true)
|
||||
// remoteVideoTrack?.let {
|
||||
// it.setEnabled(true)
|
||||
// it.addSink(remoteViewRenderer)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachViewRenderersInternal() {
|
||||
executor.execute {
|
||||
audioSource = peerConnectionFactory?.createAudioSource(DEFAULT_AUDIO_CONSTRAINTS)
|
||||
localAudioTrack = peerConnectionFactory?.createAudioTrack(AUDIO_TRACK_ID, audioSource)
|
||||
localAudioTrack?.setEnabled(true)
|
||||
|
||||
localViewRenderer?.setMirror(true)
|
||||
localVideoTrack?.addSink(localViewRenderer)
|
||||
// render local video in pip view
|
||||
localSurfaceRenderer?.get()?.let { pipSurface ->
|
||||
pipSurface.setMirror(true)
|
||||
currentCall?.localVideoTrack?.addSink(pipSurface)
|
||||
}
|
||||
|
||||
localMediaStream = peerConnectionFactory?.createLocalMediaStream("ARDAMS") // magic value?
|
||||
|
||||
if (currentCall?.isVideoCall == true) {
|
||||
val cameraIterator = if (Camera2Enumerator.isSupported(context)) Camera2Enumerator(context) else Camera1Enumerator(false)
|
||||
val frontCamera = cameraIterator.deviceNames
|
||||
?.firstOrNull { cameraIterator.isFrontFacing(it) }
|
||||
?: cameraIterator.deviceNames?.first()
|
||||
|
||||
val videoCapturer = cameraIterator.createCapturer(frontCamera, null)
|
||||
|
||||
videoSource = peerConnectionFactory?.createVideoSource(videoCapturer.isScreencast)
|
||||
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext)
|
||||
Timber.v("## VOIP Local video source created")
|
||||
videoCapturer.initialize(surfaceTextureHelper, context.applicationContext, videoSource!!.capturerObserver)
|
||||
videoCapturer.startCapture(1280, 720, 30)
|
||||
localVideoTrack = peerConnectionFactory?.createVideoTrack("ARDAMSv0", videoSource)
|
||||
Timber.v("## VOIP Local video track created")
|
||||
localSurfaceRenderer?.get()?.let { surface ->
|
||||
localVideoTrack?.addSink(surface)
|
||||
}
|
||||
localVideoTrack?.setEnabled(true)
|
||||
|
||||
localVideoTrack?.addSink(localViewRenderer)
|
||||
localMediaStream?.addTrack(localVideoTrack)
|
||||
remoteVideoTrack?.let {
|
||||
it.setEnabled(true)
|
||||
it.addSink(remoteViewRenderer)
|
||||
}
|
||||
// If remote track exists, then sink it to surface
|
||||
remoteSurfaceRenderer?.get()?.let { participantSurface ->
|
||||
currentCall?.remoteVideoTrack?.let {
|
||||
it.setEnabled(true)
|
||||
it.addSink(participantSurface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
localMediaStream?.addTrack(localAudioTrack)
|
||||
|
||||
Timber.v("## VOIP add local stream to peer connection")
|
||||
peerConnection?.addStream(localMediaStream)
|
||||
fun acceptIncomingCall() {
|
||||
Timber.v("## VOIP acceptIncomingCall from state ${currentCall?.mxCall?.state}")
|
||||
if (currentCall?.mxCall?.state == CallState.LOCAL_RINGING) {
|
||||
getTurnServer { turnServer ->
|
||||
internalAcceptIncomingCall(currentCall!!, turnServer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun detachRenderers() {
|
||||
Timber.v("## VOIP detachRenderers")
|
||||
//currentCall?.localMediaStream?.let { currentCall?.peerConnection?.removeStream(it) }
|
||||
localSurfaceRenderer?.get()?.let {
|
||||
localVideoTrack?.removeSink(it)
|
||||
currentCall?.localVideoTrack?.removeSink(it)
|
||||
}
|
||||
remoteSurfaceRenderer?.get()?.let {
|
||||
remoteVideoTrack?.removeSink(it)
|
||||
currentCall?.remoteVideoTrack?.removeSink(it)
|
||||
}
|
||||
localSurfaceRenderer = null
|
||||
remoteSurfaceRenderer = null
|
||||
}
|
||||
|
||||
fun close() {
|
||||
CallService.onNoActiveCall(context)
|
||||
executor.execute {
|
||||
// Do not dispose peer connection (https://bugs.chromium.org/p/webrtc/issues/detail?id=7543)
|
||||
tryThis { audioSource?.dispose() }
|
||||
tryThis { videoSource?.dispose() }
|
||||
tryThis { videoCapturer?.stopCapture() }
|
||||
tryThis { videoCapturer?.dispose() }
|
||||
localMediaStream?.let { peerConnection?.removeStream(it) }
|
||||
peerConnection?.close()
|
||||
peerConnection = null
|
||||
peerConnectionFactory?.stopAecDump()
|
||||
peerConnectionFactory = null
|
||||
currentCall?.release()
|
||||
videoCapturer?.stopCapture()
|
||||
videoCapturer?.dispose()
|
||||
videoCapturer = null
|
||||
}
|
||||
iceCandidateDisposable?.dispose()
|
||||
context.stopService(Intent(context, CallHeadsUpService::class.java))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -305,126 +440,134 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
|||
}
|
||||
|
||||
fun startOutgoingCall(context: Context, signalingRoomId: String, otherUserId: String, isVideoCall: Boolean) {
|
||||
Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall")
|
||||
val createdCall = sessionHolder.getSafeActiveSession()?.callService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
|
||||
currentCall = createdCall
|
||||
val callContext = CallContext(createdCall)
|
||||
currentCall = callContext
|
||||
|
||||
startHeadsUpService(createdCall)
|
||||
executor.execute {
|
||||
callContext.remoteCandidateSource = ReplaySubject.create()
|
||||
}
|
||||
|
||||
// start the activity now
|
||||
context.startActivity(VectorCallActivity.newIntent(context, createdCall))
|
||||
}
|
||||
|
||||
sessionHolder.getActiveSession().callService().getTurnServer(object : MatrixCallback<TurnServer?> {
|
||||
override fun onSuccess(data: TurnServer?) {
|
||||
startCall(data)
|
||||
sendSdpOffer()
|
||||
override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) {
|
||||
Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId}")
|
||||
if (currentCall?.mxCall?.callId != mxCall.callId) return Unit.also {
|
||||
Timber.w("## VOIP ignore ice candidates from other call")
|
||||
}
|
||||
val callContext = currentCall ?: return
|
||||
|
||||
executor.execute {
|
||||
iceCandidatesContent.candidates.forEach {
|
||||
Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId} sdp: ${it.candidate}")
|
||||
val iceCandidate = IceCandidate(it.sdpMid, it.sdpMLineIndex, it.candidate)
|
||||
callContext.remoteCandidateSource?.onNext(iceCandidate)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) {
|
||||
Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}")
|
||||
// TODO What if a call is currently active?
|
||||
if (currentCall != null) {
|
||||
Timber.w("TODO: Automatically reject incoming call?")
|
||||
Timber.w("## VOIP TODO: Automatically reject incoming call?")
|
||||
mxCall.hangUp()
|
||||
return
|
||||
}
|
||||
|
||||
currentCall = mxCall
|
||||
|
||||
startHeadsUpService(mxCall)
|
||||
|
||||
sessionHolder.getActiveSession().callService().getTurnServer(object : MatrixCallback<TurnServer?> {
|
||||
override fun onSuccess(data: TurnServer?) {
|
||||
startCall(data)
|
||||
setInviteRemoteDescription(callInviteContent.offer?.sdp)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setInviteRemoteDescription(description: String?) {
|
||||
val callContext = CallContext(mxCall)
|
||||
currentCall = callContext
|
||||
executor.execute {
|
||||
val sdp = SessionDescription(SessionDescription.Type.OFFER, description)
|
||||
peerConnection?.setRemoteDescription(sdpObserver, sdp)
|
||||
callContext.remoteCandidateSource = ReplaySubject.create()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startHeadsUpService(mxCall: MxCallDetail) {
|
||||
val callHeadsUpServiceIntent = CallHeadsUpService.newInstance(context, mxCall)
|
||||
ContextCompat.startForegroundService(context, callHeadsUpServiceIntent)
|
||||
CallService.onIncomingCall(context,
|
||||
mxCall.isVideoCall,
|
||||
mxCall.otherUserId,
|
||||
mxCall.roomId,
|
||||
sessionHolder.getSafeActiveSession()?.myUserId ?: "",
|
||||
mxCall.callId)
|
||||
|
||||
context.bindService(Intent(context, CallHeadsUpService::class.java), serviceConnection, 0)
|
||||
}
|
||||
|
||||
fun answerCall() {
|
||||
if (currentCall != null) {
|
||||
executor.execute { createAnswer() }
|
||||
}
|
||||
callContext.offerSdp = callInviteContent.offer
|
||||
}
|
||||
|
||||
private fun createAnswer() {
|
||||
Timber.w("## VOIP createAnswer")
|
||||
val call = currentCall ?: return
|
||||
val constraints = MediaConstraints().apply {
|
||||
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
|
||||
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (currentCall?.isVideoCall == true) "true" else "false"))
|
||||
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (call.mxCall.isVideoCall) "true" else "false"))
|
||||
}
|
||||
executor.execute {
|
||||
call.peerConnection?.createAnswer(object : SdpObserverAdapter() {
|
||||
|
||||
peerConnection?.createAnswer(sdpObserver, constraints)
|
||||
override fun onCreateSuccess(p0: SessionDescription?) {
|
||||
if (p0 == null) return
|
||||
call.peerConnection?.setLocalDescription(object : SdpObserverAdapter() {}, p0)
|
||||
// Now need to send it
|
||||
call.mxCall.accept(p0)
|
||||
}
|
||||
}, constraints)
|
||||
}
|
||||
}
|
||||
|
||||
fun endCall() {
|
||||
currentCall?.hangUp()
|
||||
currentCall?.mxCall?.hangUp()
|
||||
currentCall = null
|
||||
close()
|
||||
}
|
||||
|
||||
override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
|
||||
val call = currentCall ?: return
|
||||
if (call.mxCall.callId != callAnswerContent.callId) return Unit.also {
|
||||
Timber.w("onCallAnswerReceived for non active call? ${callAnswerContent.callId}")
|
||||
}
|
||||
executor.execute {
|
||||
Timber.v("## answerReceived")
|
||||
Timber.v("## VOIP onCallAnswerReceived ${callAnswerContent.callId}")
|
||||
val sdp = SessionDescription(SessionDescription.Type.ANSWER, callAnswerContent.answer.sdp)
|
||||
peerConnection?.setRemoteDescription(sdpObserver, sdp)
|
||||
call.peerConnection?.setRemoteDescription(object : SdpObserverAdapter() {
|
||||
|
||||
}, sdp)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallHangupReceived(callHangupContent: CallHangupContent) {
|
||||
val call = currentCall ?: return
|
||||
if (call.mxCall.callId != callHangupContent.callId) return Unit.also {
|
||||
Timber.w("onCallHangupReceived for non active call? ${callHangupContent.callId}")
|
||||
}
|
||||
call.mxCall.state = CallState.TERMINATED
|
||||
currentCall = null
|
||||
close()
|
||||
}
|
||||
|
||||
private inner class SdpObserver : org.webrtc.SdpObserver {
|
||||
private inner class StreamObserver(val callContext: CallContext) : PeerConnection.Observer {
|
||||
|
||||
override fun onCreateSuccess(origSdp: SessionDescription) {
|
||||
Timber.v("## VOIP SdpObserver onCreateSuccess")
|
||||
if (localSdp != null) return
|
||||
override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) {
|
||||
Timber.v("## VOIP StreamObserver onConnectionChange: $newState")
|
||||
when (newState) {
|
||||
PeerConnection.PeerConnectionState.CONNECTED -> {
|
||||
callContext.mxCall.state = CallState.CONNECTED
|
||||
}
|
||||
PeerConnection.PeerConnectionState.FAILED -> {
|
||||
endCall()
|
||||
}
|
||||
PeerConnection.PeerConnectionState.NEW,
|
||||
PeerConnection.PeerConnectionState.CONNECTING,
|
||||
PeerConnection.PeerConnectionState.DISCONNECTED,
|
||||
PeerConnection.PeerConnectionState.CLOSED,
|
||||
null -> {
|
||||
|
||||
executor.execute {
|
||||
localSdp = SessionDescription(origSdp.type, origSdp.description)
|
||||
peerConnection?.setLocalDescription(sdpObserver, localSdp)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSetSuccess() {
|
||||
Timber.v("## VOIP SdpObserver onSetSuccess")
|
||||
executor.execute {
|
||||
localSdp?.let {
|
||||
if (currentCall?.isOutgoing == true) {
|
||||
currentCall?.offerSdp(it)
|
||||
} else {
|
||||
currentCall?.accept(it)
|
||||
currentCall?.let { context.startActivity(VectorCallActivity.newIntent(context, it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateFailure(error: String) {
|
||||
Timber.v("## VOIP SdpObserver onCreateFailure: $error")
|
||||
}
|
||||
|
||||
override fun onSetFailure(error: String) {
|
||||
Timber.v("## VOIP SdpObserver onSetFailure: $error")
|
||||
}
|
||||
}
|
||||
|
||||
private inner class StreamObserver : PeerConnection.Observer {
|
||||
override fun onIceCandidate(iceCandidate: IceCandidate) {
|
||||
Timber.v("## VOIP StreamObserver onIceCandidate: $iceCandidate")
|
||||
iceCandidateSource.onNext(iceCandidate)
|
||||
callContext.iceCandidateSource.onNext(iceCandidate)
|
||||
}
|
||||
|
||||
override fun onDataChannel(dc: DataChannel) {
|
||||
|
@ -450,12 +593,16 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
|||
override fun onAddStream(stream: MediaStream) {
|
||||
Timber.v("## VOIP StreamObserver onAddStream: $stream")
|
||||
executor.execute {
|
||||
|
||||
// reportError("Weird-looking stream: " + stream);
|
||||
if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) return@execute
|
||||
|
||||
if (stream.videoTracks.size == 1) {
|
||||
remoteVideoTrack = stream.videoTracks.first()
|
||||
remoteVideoTrack?.setEnabled(true)
|
||||
remoteViewRenderer?.let { remoteVideoTrack?.addSink(it) }
|
||||
val remoteVideoTrack = stream.videoTracks.first()
|
||||
remoteVideoTrack.setEnabled(true)
|
||||
callContext.remoteVideoTrack = remoteVideoTrack
|
||||
// sink to renderer if attached
|
||||
remoteSurfaceRenderer?.get().let { remoteVideoTrack.addSink(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -464,9 +611,9 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
|||
Timber.v("## VOIP StreamObserver onRemoveStream")
|
||||
executor.execute {
|
||||
remoteSurfaceRenderer?.get()?.let {
|
||||
remoteVideoTrack?.removeSink(it)
|
||||
callContext.remoteVideoTrack?.removeSink(it)
|
||||
}
|
||||
remoteVideoTrack = null
|
||||
callContext.remoteVideoTrack = null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -484,8 +631,16 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
|||
|
||||
override fun onRenegotiationNeeded() {
|
||||
Timber.v("## VOIP StreamObserver onRenegotiationNeeded")
|
||||
// Should not do anything, for now we follow a pre-agreed-upon
|
||||
// signaling/negotiation protocol.
|
||||
}
|
||||
|
||||
/**
|
||||
* This happens when a new track of any kind is added to the media stream.
|
||||
* This event is fired when the browser adds a track to the stream
|
||||
* (such as when a RTCPeerConnection is renegotiated or a stream being captured using HTMLMediaElement.captureStream()
|
||||
* gets a new set of tracks because the media element being captured loaded a new source.
|
||||
*/
|
||||
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
|
||||
Timber.v("## VOIP StreamObserver onAddTrack")
|
||||
}
|
||||
|
|
|
@ -21,34 +21,44 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import im.vector.riotx.core.di.HasVectorInjector
|
||||
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.riotx.features.notifications.NotificationUtils
|
||||
import im.vector.riotx.features.settings.VectorLocale.context
|
||||
import timber.log.Timber
|
||||
|
||||
class CallHeadsUpActionReceiver : BroadcastReceiver() {
|
||||
|
||||
private lateinit var peerConnectionManager: WebRtcPeerConnectionManager
|
||||
private lateinit var notificationUtils: NotificationUtils
|
||||
|
||||
init {
|
||||
val appContext = context.applicationContext
|
||||
if (appContext is HasVectorInjector) {
|
||||
peerConnectionManager = appContext.injector().webRtcPeerConnectionManager()
|
||||
notificationUtils = appContext.injector().notificationUtils()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
when (intent?.getIntExtra(CallHeadsUpService.EXTRA_CALL_ACTION_KEY, 0)) {
|
||||
CallHeadsUpService.CALL_ACTION_ANSWER -> onCallAnswerClicked()
|
||||
CallHeadsUpService.CALL_ACTION_REJECT -> onCallRejectClicked()
|
||||
}
|
||||
// when (intent?.getIntExtra(CallHeadsUpService.EXTRA_CALL_ACTION_KEY, 0)) {
|
||||
// CallHeadsUpService.CALL_ACTION_ANSWER -> onCallAnswerClicked(context)
|
||||
// CallHeadsUpService.CALL_ACTION_REJECT -> onCallRejectClicked()
|
||||
// }
|
||||
|
||||
// Not sure why this should be needed
|
||||
// val it = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
|
||||
// context.sendBroadcast(it)
|
||||
|
||||
// Close the notification after the click action is performed.
|
||||
// context.stopService(Intent(context, CallHeadsUpService::class.java))
|
||||
}
|
||||
|
||||
private fun onCallRejectClicked() {
|
||||
Timber.d("onCallRejectClicked")
|
||||
peerConnectionManager.endCall()
|
||||
}
|
||||
// private fun onCallRejectClicked() {
|
||||
// Timber.d("onCallRejectClicked")
|
||||
// peerConnectionManager.endCall()
|
||||
// }
|
||||
|
||||
private fun onCallAnswerClicked() {
|
||||
Timber.d("onCallAnswerClicked")
|
||||
peerConnectionManager.answerCall()
|
||||
}
|
||||
// private fun onCallAnswerClicked(context: Context) {
|
||||
// Timber.d("onCallAnswerClicked")
|
||||
// peerConnectionManager.answerCall(context)
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -1,144 +1,180 @@
|
|||
/*
|
||||
* 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.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.ContentResolver.SCHEME_ANDROID_RESOURCE
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.AudioAttributes
|
||||
import android.net.Uri
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import im.vector.matrix.android.api.session.call.MxCallDetail
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.features.call.VectorCallActivity
|
||||
|
||||
class CallHeadsUpService : Service() {
|
||||
|
||||
private val CHANNEL_ID = "CallChannel"
|
||||
private val CHANNEL_NAME = "Call Channel"
|
||||
private val CHANNEL_DESCRIPTION = "Call Notifications"
|
||||
|
||||
private val binder: IBinder = CallHeadsUpServiceBinder()
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val callHeadsUpServiceArgs: CallHeadsUpServiceArgs? = intent?.extras?.getParcelable(EXTRA_CALL_HEADS_UP_SERVICE_PARAMS)
|
||||
|
||||
createNotificationChannel()
|
||||
|
||||
val title = callHeadsUpServiceArgs?.otherUserId ?: ""
|
||||
val description = when {
|
||||
callHeadsUpServiceArgs?.isIncomingCall == false -> getString(R.string.call_ring)
|
||||
callHeadsUpServiceArgs?.isVideoCall == true -> getString(R.string.incoming_video_call)
|
||||
else -> getString(R.string.incoming_voice_call)
|
||||
}
|
||||
|
||||
val actions = if (callHeadsUpServiceArgs?.isIncomingCall == true) createAnswerAndRejectActions() else emptyList()
|
||||
|
||||
createNotification(title, description, actions).also {
|
||||
startForeground(NOTIFICATION_ID, it)
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun createNotification(title: String, content: String, actions: List<NotificationCompat.Action>): Notification {
|
||||
return NotificationCompat
|
||||
.Builder(applicationContext, CHANNEL_ID)
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setSmallIcon(R.drawable.ic_call)
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setDefaults(NotificationCompat.DEFAULT_SOUND or NotificationCompat.DEFAULT_VIBRATE)
|
||||
.setSound(Uri.parse(SCHEME_ANDROID_RESOURCE + "://" + applicationContext.packageName + "/raw/ring.ogg"))
|
||||
.setVibrate(longArrayOf(1000, 1000))
|
||||
.setFullScreenIntent(PendingIntent.getActivity(applicationContext, 0, Intent(applicationContext, VectorCallActivity::class.java), 0), true)
|
||||
.setOngoing(true)
|
||||
.apply { actions.forEach { addAction(it) } }
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createAnswerAndRejectActions(): List<NotificationCompat.Action> {
|
||||
val answerCallActionReceiver = Intent(applicationContext, CallHeadsUpActionReceiver::class.java).apply {
|
||||
putExtra(EXTRA_CALL_ACTION_KEY, CALL_ACTION_ANSWER)
|
||||
}
|
||||
val rejectCallActionReceiver = Intent(applicationContext, CallHeadsUpActionReceiver::class.java).apply {
|
||||
putExtra(EXTRA_CALL_ACTION_KEY, CALL_ACTION_REJECT)
|
||||
}
|
||||
val answerCallPendingIntent = PendingIntent.getBroadcast(applicationContext, CALL_ACTION_ANSWER, answerCallActionReceiver, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val rejectCallPendingIntent = PendingIntent.getBroadcast(applicationContext, CALL_ACTION_REJECT, rejectCallActionReceiver, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
return listOf(
|
||||
NotificationCompat.Action(R.drawable.ic_call, getString(R.string.call_notification_answer), answerCallPendingIntent),
|
||||
NotificationCompat.Action(R.drawable.vector_notification_reject_invitation, getString(R.string.call_notification_reject), rejectCallPendingIntent)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
|
||||
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH).apply {
|
||||
description = CHANNEL_DESCRIPTION
|
||||
setSound(
|
||||
Uri.parse(SCHEME_ANDROID_RESOURCE + "://" + applicationContext.packageName + "/raw/ring.ogg"),
|
||||
AudioAttributes
|
||||
.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
||||
.build()
|
||||
)
|
||||
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
||||
enableVibration(true)
|
||||
enableLights(true)
|
||||
}
|
||||
applicationContext.getSystemService(NotificationManager::class.java)?.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
inner class CallHeadsUpServiceBinder : Binder() {
|
||||
|
||||
fun getService() = this@CallHeadsUpService
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_CALL_HEADS_UP_SERVICE_PARAMS = "EXTRA_CALL_PARAMS"
|
||||
|
||||
const val EXTRA_CALL_ACTION_KEY = "EXTRA_CALL_ACTION_KEY"
|
||||
const val CALL_ACTION_ANSWER = 100
|
||||
const val CALL_ACTION_REJECT = 101
|
||||
|
||||
private const val NOTIFICATION_ID = 999
|
||||
|
||||
fun newInstance(context: Context, mxCall: MxCallDetail): Intent {
|
||||
val args = CallHeadsUpServiceArgs(mxCall.roomId, mxCall.otherUserId, !mxCall.isOutgoing, mxCall.isVideoCall)
|
||||
return Intent(context, CallHeadsUpService::class.java).apply {
|
||||
putExtra(EXTRA_CALL_HEADS_UP_SERVICE_PARAMS, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
///*
|
||||
// * 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.service
|
||||
//
|
||||
//import android.app.Notification
|
||||
//import android.app.NotificationChannel
|
||||
//import android.app.NotificationManager
|
||||
//import android.app.PendingIntent
|
||||
//import android.app.Service
|
||||
//import android.content.ContentResolver.SCHEME_ANDROID_RESOURCE
|
||||
//import android.content.Context
|
||||
//import android.content.Intent
|
||||
//import android.media.AudioAttributes
|
||||
//import android.net.Uri
|
||||
//import android.os.Binder
|
||||
//import android.os.Build
|
||||
//import android.os.IBinder
|
||||
//import androidx.core.app.NotificationCompat
|
||||
//import androidx.core.content.ContextCompat
|
||||
//import androidx.core.graphics.drawable.IconCompat
|
||||
//import im.vector.matrix.android.api.session.call.MxCallDetail
|
||||
//import im.vector.riotx.R
|
||||
//import im.vector.riotx.core.extensions.vectorComponent
|
||||
//import im.vector.riotx.features.call.VectorCallActivity
|
||||
//import im.vector.riotx.features.notifications.NotificationUtils
|
||||
//import im.vector.riotx.features.themes.ThemeUtils
|
||||
//
|
||||
//class CallHeadsUpService : Service() {
|
||||
////
|
||||
//// private val CHANNEL_ID = "CallChannel"
|
||||
//// private val CHANNEL_NAME = "Call Channel"
|
||||
//// private val CHANNEL_DESCRIPTION = "Call Notifications"
|
||||
//
|
||||
// lateinit var notificationUtils: NotificationUtils
|
||||
// private val binder: IBinder = CallHeadsUpServiceBinder()
|
||||
//
|
||||
// override fun onBind(intent: Intent): IBinder? {
|
||||
// return binder
|
||||
// }
|
||||
//
|
||||
// override fun onCreate() {
|
||||
// super.onCreate()
|
||||
// notificationUtils = vectorComponent().notificationUtils()
|
||||
// }
|
||||
// override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
// val callHeadsUpServiceArgs: CallHeadsUpServiceArgs? = intent?.extras?.getParcelable(EXTRA_CALL_HEADS_UP_SERVICE_PARAMS)
|
||||
//
|
||||
//// createNotificationChannel()
|
||||
//
|
||||
//// val title = callHeadsUpServiceArgs?.otherUserId ?: ""
|
||||
//// val description = when {
|
||||
//// callHeadsUpServiceArgs?.isIncomingCall == false -> getString(R.string.call_ring)
|
||||
//// callHeadsUpServiceArgs?.isVideoCall == true -> getString(R.string.incoming_video_call)
|
||||
//// else -> getString(R.string.incoming_voice_call)
|
||||
//// }
|
||||
//
|
||||
// // val actions = if (callHeadsUpServiceArgs?.isIncomingCall == true) createAnswerAndRejectActions() else emptyList()
|
||||
//
|
||||
// notificationUtils.buildIncomingCallNotification(
|
||||
// callHeadsUpServiceArgs?.isVideoCall ?: false,
|
||||
// callHeadsUpServiceArgs?.otherUserId ?: "",
|
||||
// callHeadsUpServiceArgs?.roomId ?: "",
|
||||
// callHeadsUpServiceArgs?.callId ?: ""
|
||||
// ).let {
|
||||
// startForeground(NOTIFICATION_ID, it)
|
||||
// }
|
||||
//// createNotification(title, description, actions).also {
|
||||
//// startForeground(NOTIFICATION_ID, it)
|
||||
//// }
|
||||
//
|
||||
// return START_STICKY
|
||||
// }
|
||||
//
|
||||
//// private fun createNotification(title: String, content: String, actions: List<NotificationCompat.Action>): Notification {
|
||||
//// val answerCallActionReceiver = Intent(applicationContext, CallHeadsUpActionReceiver::class.java).apply {
|
||||
//// putExtra(EXTRA_CALL_ACTION_KEY, CALL_ACTION_ANSWER)
|
||||
//// }.let {
|
||||
//// PendingIntent.getBroadcast(applicationContext, CALL_ACTION_ANSWER, it, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
//// }
|
||||
//// return NotificationCompat
|
||||
//// .Builder(applicationContext, CHANNEL_ID)
|
||||
//// .setContentTitle(title)
|
||||
//// .setContentText(content)
|
||||
//// .setSmallIcon(R.drawable.ic_call)
|
||||
//// .setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
//// .setWhen(0)
|
||||
//// .setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
//// .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
//// .setDefaults(NotificationCompat.DEFAULT_SOUND or NotificationCompat.DEFAULT_VIBRATE)
|
||||
//// .setSound(Uri.parse(SCHEME_ANDROID_RESOURCE + "://" + applicationContext.packageName + "/raw/ring.ogg"))
|
||||
//// .setVibrate(longArrayOf(1000, 1000))
|
||||
//// .setFullScreenIntent(answerCallActionReceiver, true)
|
||||
//// .setOngoing(true)
|
||||
//// //.setStyle(NotificationCompat.BigTextStyle())
|
||||
//// .setAutoCancel(true)
|
||||
//// .apply { actions.forEach { addAction(it) } }
|
||||
//// .build()
|
||||
//// }
|
||||
//
|
||||
//// private fun createAnswerAndRejectActions(): List<NotificationCompat.Action> {
|
||||
//// val answerCallActionReceiver = Intent(applicationContext, CallHeadsUpActionReceiver::class.java).apply {
|
||||
//// putExtra(EXTRA_CALL_ACTION_KEY, CALL_ACTION_ANSWER)
|
||||
//// }
|
||||
//// val rejectCallActionReceiver = Intent(applicationContext, CallHeadsUpActionReceiver::class.java).apply {
|
||||
//// putExtra(EXTRA_CALL_ACTION_KEY, CALL_ACTION_REJECT)
|
||||
//// }
|
||||
//// val answerCallPendingIntent = PendingIntent.getBroadcast(applicationContext, CALL_ACTION_ANSWER, answerCallActionReceiver, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
//// val rejectCallPendingIntent = PendingIntent.getBroadcast(applicationContext, CALL_ACTION_REJECT, rejectCallActionReceiver, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
////
|
||||
//// return listOf(
|
||||
//// NotificationCompat.Action(
|
||||
//// R.drawable.ic_call,
|
||||
//// //IconCompat.createWithResource(applicationContext, R.drawable.ic_call).setTint(ContextCompat.getColor(applicationContext, R.color.riotx_positive_accent)),
|
||||
//// getString(R.string.call_notification_answer),
|
||||
//// answerCallPendingIntent
|
||||
//// ),
|
||||
//// NotificationCompat.Action(
|
||||
//// IconCompat.createWithResource(applicationContext, R.drawable.ic_call_end).setTint(ContextCompat.getColor(applicationContext, R.color.riotx_notice)),
|
||||
//// getString(R.string.call_notification_reject),
|
||||
//// rejectCallPendingIntent)
|
||||
//// )
|
||||
//// }
|
||||
//
|
||||
//// private fun createNotificationChannel() {
|
||||
//// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
////
|
||||
//// val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH).apply {
|
||||
//// description = CHANNEL_DESCRIPTION
|
||||
//// setSound(
|
||||
//// Uri.parse(SCHEME_ANDROID_RESOURCE + "://" + applicationContext.packageName + "/raw/ring.ogg"),
|
||||
//// AudioAttributes
|
||||
//// .Builder()
|
||||
//// .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
//// .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
||||
//// .build()
|
||||
//// )
|
||||
//// lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
||||
//// enableVibration(true)
|
||||
//// enableLights(true)
|
||||
//// }
|
||||
//// applicationContext.getSystemService(NotificationManager::class.java)?.createNotificationChannel(channel)
|
||||
//// }
|
||||
//
|
||||
// inner class CallHeadsUpServiceBinder : Binder() {
|
||||
//
|
||||
// fun getService() = this@CallHeadsUpService
|
||||
// }
|
||||
//
|
||||
//
|
||||
// companion object {
|
||||
// private const val EXTRA_CALL_HEADS_UP_SERVICE_PARAMS = "EXTRA_CALL_PARAMS"
|
||||
//
|
||||
// const val EXTRA_CALL_ACTION_KEY = "EXTRA_CALL_ACTION_KEY"
|
||||
//// const val CALL_ACTION_ANSWER = 100
|
||||
// const val CALL_ACTION_REJECT = 101
|
||||
//
|
||||
// private const val NOTIFICATION_ID = 999
|
||||
//
|
||||
// fun newInstance(context: Context, mxCall: MxCallDetail): Intent {
|
||||
// val args = CallHeadsUpServiceArgs(mxCall.callId, mxCall.roomId, mxCall.otherUserId, !mxCall.isOutgoing, mxCall.isVideoCall)
|
||||
// return Intent(context, CallHeadsUpService::class.java).apply {
|
||||
// putExtra(EXTRA_CALL_HEADS_UP_SERVICE_PARAMS, args)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -21,6 +21,7 @@ import kotlinx.android.parcel.Parcelize
|
|||
|
||||
@Parcelize
|
||||
data class CallHeadsUpServiceArgs(
|
||||
val callId: String,
|
||||
val roomId: String,
|
||||
val otherUserId: String,
|
||||
val isIncomingCall: Boolean,
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.telecom
|
||||
|
||||
import android.content.Context
|
||||
import android.telephony.TelephonyManager
|
||||
|
||||
object TelecomUtils {
|
||||
|
||||
fun isLineBusy(context: Context): Boolean {
|
||||
val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
|
||||
?: return false
|
||||
return telephonyManager.callState != TelephonyManager.CALL_STATE_IDLE
|
||||
}
|
||||
}
|
|
@ -48,6 +48,7 @@ import im.vector.riotx.core.extensions.configureAndStart
|
|||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.riotx.features.notifications.PushRuleTriggerListener
|
||||
import im.vector.riotx.features.session.SessionListener
|
||||
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
|
||||
|
@ -66,7 +67,8 @@ class LoginViewModel @AssistedInject constructor(
|
|||
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
|
||||
private val sessionListener: SessionListener,
|
||||
private val reAuthHelper: ReAuthHelper,
|
||||
private val stringProvider: StringProvider)
|
||||
private val stringProvider: StringProvider,
|
||||
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager)
|
||||
: VectorViewModel<LoginViewState, LoginAction, LoginViewEvents>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
|
@ -613,6 +615,7 @@ class LoginViewModel @AssistedInject constructor(
|
|||
private fun onSessionCreated(session: Session) {
|
||||
activeSessionHolder.setActiveSession(session)
|
||||
session.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
|
||||
session.callService().addCallListener(webRtcPeerConnectionManager)
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Success(Unit)
|
||||
|
|
|
@ -35,11 +35,14 @@ import androidx.core.app.NotificationManagerCompat
|
|||
import androidx.core.app.RemoteInput
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.riotx.BuildConfig
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.core.utils.startNotificationChannelSettingsIntent
|
||||
import im.vector.riotx.features.call.VectorCallActivity
|
||||
import im.vector.riotx.features.call.service.CallHeadsUpActionReceiver
|
||||
import im.vector.riotx.features.home.HomeActivity
|
||||
import im.vector.riotx.features.home.room.detail.RoomDetailActivity
|
||||
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
|
||||
|
@ -263,13 +266,13 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun buildIncomingCallNotification(isVideo: Boolean,
|
||||
roomName: String,
|
||||
matrixId: String,
|
||||
otherUserId: String,
|
||||
roomId: String,
|
||||
callId: String): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
|
||||
val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle(ensureTitleNotEmpty(roomName))
|
||||
.setContentTitle(ensureTitleNotEmpty(otherUserId))
|
||||
.apply {
|
||||
if (isVideo) {
|
||||
setContentText(stringProvider.getString(R.string.incoming_video_call))
|
||||
|
@ -282,26 +285,61 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
.setLights(accentColor, 500, 500)
|
||||
|
||||
// Compat: Display the incoming call notification on the lock screen
|
||||
builder.priority = NotificationCompat.PRIORITY_MAX
|
||||
builder.priority = NotificationCompat.PRIORITY_HIGH
|
||||
|
||||
// clear the activity stack to home activity
|
||||
val intent = Intent(context, HomeActivity::class.java)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
// val intent = Intent(context, HomeActivity::class.java)
|
||||
// .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
// TODO .putExtra(VectorHomeActivity.EXTRA_CALL_SESSION_ID, matrixId)
|
||||
// TODO .putExtra(VectorHomeActivity.EXTRA_CALL_ID, callId)
|
||||
|
||||
// Recreate the back stack
|
||||
val stackBuilder = TaskStackBuilder.create(context)
|
||||
.addParentStack(HomeActivity::class.java)
|
||||
.addNextIntent(intent)
|
||||
// val stackBuilder = TaskStackBuilder.create(context)
|
||||
// .addParentStack(HomeActivity::class.java)
|
||||
// .addNextIntent(intent)
|
||||
|
||||
// android 4.3 issue
|
||||
// use a generator for the private requestCode.
|
||||
// When using 0, the intent is not created/launched when the user taps on the notification.
|
||||
//
|
||||
val pendingIntent = stackBuilder.getPendingIntent(Random.nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val requestId = Random.nextInt(1000)
|
||||
// val pendingIntent = stackBuilder.getPendingIntent(requestId, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val contentPendingIntent = TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java))
|
||||
.addNextIntent(VectorCallActivity.newIntent(context, callId, roomId, otherUserId, true, isVideo, false, VectorCallActivity.INCOMING_RINGING))
|
||||
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
builder.setContentIntent(pendingIntent)
|
||||
val answerCallPendingIntent = TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java))
|
||||
.addNextIntent(VectorCallActivity.newIntent(context, callId, roomId, otherUserId, true, isVideo, true, VectorCallActivity.INCOMING_ACCEPT))
|
||||
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
// val answerCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply {
|
||||
// putExtra(CallHeadsUpService.EXTRA_CALL_ACTION_KEY, CallHeadsUpService.CALL_ACTION_ANSWER)
|
||||
// }
|
||||
val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply {
|
||||
// putExtra(CallHeadsUpService.EXTRA_CALL_ACTION_KEY, CallHeadsUpService.CALL_ACTION_REJECT)
|
||||
}
|
||||
//val answerCallPendingIntent = PendingIntent.getBroadcast(context, requestId, answerCallActionReceiver, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val rejectCallPendingIntent = PendingIntent.getBroadcast(context, requestId + 1, rejectCallActionReceiver, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
builder.addAction(
|
||||
NotificationCompat.Action(
|
||||
R.drawable.ic_call,
|
||||
//IconCompat.createWithResource(applicationContext, R.drawable.ic_call).setTint(ContextCompat.getColor(applicationContext, R.color.riotx_positive_accent)),
|
||||
context.getString(R.string.call_notification_answer),
|
||||
answerCallPendingIntent
|
||||
)
|
||||
)
|
||||
|
||||
builder.addAction(
|
||||
NotificationCompat.Action(
|
||||
IconCompat.createWithResource(context, R.drawable.ic_call_end).setTint(ContextCompat.getColor(context, R.color.riotx_notice)),
|
||||
context.getString(R.string.call_notification_reject),
|
||||
rejectCallPendingIntent)
|
||||
)
|
||||
|
||||
builder.setContentIntent(contentPendingIntent)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
@ -334,10 +372,18 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
.setSmallIcon(R.drawable.incoming_call_notification_transparent)
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
|
||||
// Display the pending call notification on the lock screen
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
builder.priority = NotificationCompat.PRIORITY_MAX
|
||||
}
|
||||
builder.priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
|
||||
val contentPendingIntent = TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java))
|
||||
//TODO other userId
|
||||
.addNextIntent(VectorCallActivity.newIntent(context, callId, roomId, "otherUserId", true, isVideo, false, null))
|
||||
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
// android 4.3 issue
|
||||
// use a generator for the private requestCode.
|
||||
// When using 0, the intent is not created/launched when the user taps on the notification.
|
||||
builder.setContentIntent(contentPendingIntent)
|
||||
|
||||
/* TODO
|
||||
// Build the pending intent for when the notification is clicked
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M20.01,15.38c-1.23,0 -2.42,-0.2 -3.53,-0.56 -0.35,-0.12 -0.74,-0.03 -1.01,0.24l-1.57,1.97c-2.83,-1.35 -5.48,-3.9 -6.89,-6.83l1.95,-1.66c0.27,-0.28 0.35,-0.67 0.24,-1.02 -0.37,-1.11 -0.56,-2.3 -0.56,-3.53 0,-0.54 -0.45,-0.99 -0.99,-0.99H4.19C3.65,3 3,3.24 3,3.99 3,13.28 10.73,21 20.01,21c0.71,0 0.99,-0.63 0.99,-1.18v-3.45c0,-0.54 -0.45,-0.99 -0.99,-0.99z"/>
|
||||
<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>
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
<vector android:autoMirrored="true" android:height="40dp"
|
||||
android:viewportHeight="40" android:viewportWidth="40"
|
||||
android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF4B55" android:fillType="evenOdd"
|
||||
android:pathData="M20,20m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M20,17.7604C18.4,17.7604 16.85,18.0045 15.4,18.4632L15.4,21.4889C15.4,21.8696 15.17,22.2112 14.84,22.3674C13.86,22.8456 12.97,23.4605 12.18,24.1731C12,24.3487 11.75,24.4463 11.48,24.4463C11.2,24.4463 10.95,24.339 10.77,24.1633L8.29,21.7427C8.11,21.5768 8,21.3328 8,21.0595C8,20.7862 8.11,20.5422 8.29,20.3665C11.34,17.5457 15.46,15.8083 20,15.8083C24.54,15.8083 28.66,17.5457 31.71,20.3665C31.89,20.5422 32,20.7862 32,21.0595C32,21.3328 31.89,21.5768 31.71,21.7525L29.23,24.1731C29.05,24.3487 28.8,24.4561 28.52,24.4561C28.25,24.4561 28,24.3487 27.82,24.1828C27.03,23.4605 26.13,22.8554 25.15,22.3771C24.82,22.221 24.59,21.8891 24.59,21.4987L24.59,18.473C23.15,18.0044 21.6,17.7604 20,17.7604Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M5.2415,18.995L3.2268,16.7576C2.8469,16.3391 2.6614,15.7796 2.7158,15.2164C2.7702,14.6532 3.0596,14.1387 3.5127,13.799C6.0368,11.9778 8.9518,10.773 12.0235,10.2815C14.8601,9.8008 17.7666,9.9501 20.5365,10.719C23.5514,11.5274 26.332,13.0348 28.6531,15.1192C29.0664,15.5022 29.2993,16.0416 29.2949,16.6055C29.2905,17.1694 29.0492,17.706 28.6301,18.0841L26.3882,20.1028C25.6447,20.7857 24.5111,20.8127 23.7386,20.1659C22.9992,19.535 22.1907,18.99 21.3284,18.5412C20.6322,18.181 20.2102,17.4482 20.2477,16.6648L20.3438,14.863C17.5987,13.9538 14.6576,13.8027 11.8307,14.4256L11.7346,16.2273C11.6884,17.0104 11.1907,17.6959 10.46,17.9828C9.5547,18.3408 8.6926,18.8 7.8901,19.3516C7.0434,19.9224 5.9044,19.7691 5.2415,18.995Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
|
|
41
vector/src/main/res/drawable/ic_microphone_off.xml
Normal file
41
vector/src/main/res/drawable/ic_microphone_off.xml
Normal file
|
@ -0,0 +1,41 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="25dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M1,2L23,24"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M15,10.34V5C15.0007,4.256 14.725,3.5383 14.2264,2.9862C13.7277,2.4341 13.0417,2.0869 12.3015,2.0122C11.5613,1.9374 10.8197,2.1403 10.2207,2.5816C9.6217,3.0228 9.208,3.6709 9.06,4.4M9,10V13C9.0005,13.593 9.1768,14.1725 9.5064,14.6653C9.8361,15.1582 10.3045,15.5423 10.8523,15.7691C11.4002,15.996 12.0029,16.0554 12.5845,15.9399C13.1661,15.8243 13.7005,15.539 14.12,15.12L9,10Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M16.9999,17.95C16.0237,18.9464 14.7721,19.6285 13.4056,19.9086C12.039,20.1887 10.62,20.0542 9.3304,19.5223C8.0409,18.9903 6.9397,18.0853 6.1681,16.9232C5.3965,15.761 4.9897,14.3949 4.9999,13V11M18.9999,11V13C18.9996,13.4124 18.9628,13.824 18.8899,14.23"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,20V24"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M8,24H16"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
10
vector/src/main/res/drawable/ic_microphone_on.xml
Normal file
10
vector/src/main/res/drawable/ic_microphone_on.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<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="M12,0C10.9391,0 9.9217,0.4214 9.1716,1.1716C8.4214,1.9217 8,2.9391 8,4V12C8,13.0609 8.4214,14.0783 9.1716,14.8284C9.9217,15.5786 10.9391,16 12,16C13.0609,16 14.0783,15.5786 14.8284,14.8284C15.5786,14.0783 16,13.0609 16,12V4C16,2.9391 15.5786,1.9217 14.8284,1.1716C14.0783,0.4214 13.0609,0 12,0ZM10.5858,2.5858C10.9609,2.2107 11.4696,2 12,2C12.5304,2 13.0391,2.2107 13.4142,2.5858C13.7893,2.9609 14,3.4696 14,4V12C14,12.5304 13.7893,13.0391 13.4142,13.4142C13.0391,13.7893 12.5304,14 12,14C11.4696,14 10.9609,13.7893 10.5858,13.4142C10.2107,13.0391 10,12.5304 10,12V4C10,3.4696 10.2107,2.9609 10.5858,2.5858ZM6,10C6,9.4477 5.5523,9 5,9C4.4477,9 4,9.4477 4,10V12C4,14.1217 4.8429,16.1566 6.3432,17.6569C7.6058,18.9195 9.247,19.7165 11,19.9373V22H8C7.4477,22 7,22.4477 7,23C7,23.5523 7.4477,24 8,24H12H16C16.5523,24 17,23.5523 17,23C17,22.4477 16.5523,22 16,22H13V19.9373C14.753,19.7165 16.3942,18.9195 17.6569,17.6569C19.1571,16.1566 20,14.1217 20,12V10C20,9.4477 19.5523,9 19,9C18.4477,9 18,9.4477 18,10V12C18,13.5913 17.3679,15.1174 16.2426,16.2426C15.1174,17.3679 13.5913,18 12,18C10.4087,18 8.8826,17.3679 7.7574,16.2426C6.6321,15.1174 6,13.5913 6,12V10Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
18
vector/src/main/res/drawable/ic_more_vertical.xml
Normal file
18
vector/src/main/res/drawable/ic_more_vertical.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<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="M12,14C13.1046,14 14,13.1046 14,12C14,10.8954 13.1046,10 12,10C10.8954,10 10,10.8954 10,12C10,13.1046 10.8954,14 12,14Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M12,6C13.1046,6 14,5.1046 14,4C14,2.8954 13.1046,2 12,2C10.8954,2 10,2.8954 10,4C10,5.1046 10.8954,6 12,6Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M12,22C13.1046,22 14,21.1046 14,20C14,18.8954 13.1046,18 12,18C10.8954,18 10,18.8954 10,20C10,21.1046 10.8954,22 12,22Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
22
vector/src/main/res/drawable/ic_video.xml
Normal file
22
vector/src/main/res/drawable/ic_video.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<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="M23,7L16,12L23,17V7Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M1,7C1,5.8954 1.8954,5 3,5H14C15.1046,5 16,5.8954 16,7V17C16,18.1046 15.1046,19 14,19H3C1.8954,19 1,18.1046 1,17V7Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
11
vector/src/main/res/drawable/oval_destructive.xml
Normal file
11
vector/src/main/res/drawable/oval_destructive.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
|
||||
<solid android:color="@color/riotx_destructive_accent" />
|
||||
|
||||
</shape>
|
11
vector/src/main/res/drawable/oval_positive.xml
Normal file
11
vector/src/main/res/drawable/oval_positive.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
|
||||
<solid android:color="@color/riotx_positive_accent" />
|
||||
|
||||
</shape>
|
|
@ -1,11 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- tools:ignore is needed because lint thinks this can be replaced with a merge. Replacing this
|
||||
<?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. -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:background="?riotx_background"
|
||||
tools:ignore="MergeRootFrame">
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
|
@ -19,75 +19,149 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="144dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp" />
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/participantNameText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@id/callTypeText"
|
||||
tools:text="Joe" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/callTypeText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/otherMemberAvatar"
|
||||
tools:text="Video Call" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_end_call"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:src="@drawable/ic_call_end"
|
||||
android:id="@+id/otherMemberAvatar"
|
||||
android:layout_width="128dp"
|
||||
android:layout_height="128dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginBottom="32dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/layout_call_actions"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.3"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/layout_call_actions"
|
||||
<TextView
|
||||
android:id="@+id/callStatusText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginBottom="48dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:background="@drawable/bg_call_actions">
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintTop_toBottomOf="@id/otherMemberAvatar"
|
||||
tools:text="Connecting..." />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_call_speaker"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:src="@drawable/ic_call_speaker_default"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginStart="32dp"/>
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/callInfoGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
app:constraint_referenced_ids="participantNameText, callTypeText, otherMemberAvatar, callStatusText" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_call_flip_camera"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:src="@drawable/ic_call_flip_camera_default"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginStart="32dp"/>
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/callVideoGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
app:constraint_referenced_ids="pip_video_view, fullscreen_video_view"
|
||||
tools:visibility="invisible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_call_videocam_off"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:src="@drawable/ic_call_videocam_off_default"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_call_mute"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:src="@drawable/ic_call_mute_default"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginEnd="32dp"/>
|
||||
<im.vector.riotx.features.call.CallControlsView
|
||||
android:id="@+id/callControls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/iv_end_call"-->
|
||||
<!-- android:layout_width="64dp"-->
|
||||
<!-- android:layout_height="64dp"-->
|
||||
<!-- android:background="@drawable/oval_destructive"-->
|
||||
<!-- android:src="@drawable/ic_call_end"-->
|
||||
<!-- android:tint="@color/white"-->
|
||||
<!-- android:padding="8dp"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="parent"-->
|
||||
<!-- app:layout_constraintEnd_toEndOf="parent"-->
|
||||
<!-- android:layout_marginBottom="32dp"-->
|
||||
<!-- app:layout_constraintBottom_toTopOf="@+id/layout_call_actions"/>-->
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<!-- <androidx.constraintlayout.widget.ConstraintLayout-->
|
||||
<!-- android:id="@+id/layout_call_actions"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="80dp"-->
|
||||
<!-- android:layout_marginBottom="48dp"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="parent"-->
|
||||
<!-- app:layout_constraintEnd_toEndOf="parent"-->
|
||||
<!-- android:background="@drawable/bg_call_actions">-->
|
||||
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/iv_call_speaker"-->
|
||||
<!-- android:layout_width="32dp"-->
|
||||
<!-- android:layout_height="32dp"-->
|
||||
<!-- android:src="@drawable/ic_call_speaker_default"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="parent"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="parent"-->
|
||||
<!-- android:layout_marginStart="32dp"/>-->
|
||||
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/iv_call_flip_camera"-->
|
||||
<!-- android:layout_width="32dp"-->
|
||||
<!-- android:layout_height="32dp"-->
|
||||
<!-- android:src="@drawable/ic_call_flip_camera_default"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="parent"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="parent"-->
|
||||
<!-- android:layout_marginStart="32dp"/>-->
|
||||
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/iv_call_videocam_off"-->
|
||||
<!-- android:layout_width="32dp"-->
|
||||
<!-- android:layout_height="32dp"-->
|
||||
<!-- android:src="@drawable/ic_call_videocam_off_default"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="parent"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||
<!-- app:layout_constraintEnd_toEndOf="parent"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="parent" />-->
|
||||
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/iv_call_mute"-->
|
||||
<!-- android:layout_width="32dp"-->
|
||||
<!-- android:layout_height="32dp"-->
|
||||
<!-- android:src="@drawable/ic_call_mute_default"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="parent"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||
<!-- app:layout_constraintEnd_toEndOf="parent"-->
|
||||
<!-- android:layout_marginEnd="32dp"/>-->
|
||||
|
||||
<!-- </androidx.constraintlayout.widget.ConstraintLayout>-->
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/hud_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/call_fragment_container"
|
||||
|
|
207
vector/src/main/res/layout/fragment_call_controls.xml
Normal file
207
vector/src/main/res/layout/fragment_call_controls.xml
Normal file
|
@ -0,0 +1,207 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/incomingRingingControls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:orientation="horizontal"
|
||||
tools:background="@color/password_strength_bar_ok"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_icr_accept_call"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:background="@drawable/oval_positive"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_call"
|
||||
android:tint="@color/white"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_icr_end_call"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:background="@drawable/oval_destructive"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_call_end"
|
||||
android:tint="@color/white"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="iv_icr_end_call, iv_icr_accept_call"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/connectedControls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
tools:background="@color/password_strength_bar_low"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_leftMiniControl"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_home_bottom_chat"
|
||||
android:tint="?attr/riotx_background"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_mute_toggle"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:background="@drawable/oval_positive"
|
||||
android:backgroundTint="?attr/riotx_background"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_microphone_off"
|
||||
tools:src="@drawable/ic_microphone_on"
|
||||
android:tint="?attr/riotx_text_primary"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_end_call"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:background="@drawable/oval_destructive"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_call_end"
|
||||
android:tint="@color/white"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_video_toggle"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:background="@drawable/oval_positive"
|
||||
android:backgroundTint="?attr/riotx_background"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_call_videocam_off_default"
|
||||
android:tint="?attr/riotx_text_primary"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_more"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_more_vertical"
|
||||
android:tint="?attr/riotx_background"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="iv_leftMiniControl, iv_mute_toggle, iv_end_call,iv_video_toggle,iv_more"
|
||||
tools:ignore="MissingConstraints"
|
||||
tools:layout_editor_absoluteX="16dp"
|
||||
tools:layout_editor_absoluteY="16dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/iv_call_speaker"-->
|
||||
<!-- android:layout_width="32dp"-->
|
||||
<!-- android:layout_height="32dp"-->
|
||||
<!-- android:layout_marginStart="32dp"-->
|
||||
<!-- android:clickable="true"-->
|
||||
<!-- android:focusable="true"-->
|
||||
<!-- android:src="@drawable/ic_call_speaker_default"-->
|
||||
<!-- android:tint="?colorPrimary"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="parent"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="parent" />-->
|
||||
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/iv_call_flip_camera"-->
|
||||
<!-- android:layout_width="32dp"-->
|
||||
<!-- android:layout_height="32dp"-->
|
||||
<!-- android:layout_marginStart="32dp"-->
|
||||
<!-- android:clickable="true"-->
|
||||
<!-- android:focusable="true"-->
|
||||
<!-- android:src="@drawable/ic_call_flip_camera_default"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="parent"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="parent" />-->
|
||||
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/iv_end_call"-->
|
||||
<!-- android:layout_width="64dp"-->
|
||||
<!-- android:layout_height="64dp"-->
|
||||
<!-- android:layout_marginBottom="32dp"-->
|
||||
<!-- android:background="@drawable/oval_destructive"-->
|
||||
<!-- android:clickable="true"-->
|
||||
<!-- android:focusable="true"-->
|
||||
<!-- android:padding="8dp"-->
|
||||
<!-- android:src="@drawable/ic_call_end"-->
|
||||
<!-- android:tint="@color/white"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||
<!-- app:layout_constraintBottom_toTopOf="@+id/layout_call_actions"-->
|
||||
<!-- app:layout_constraintEnd_toEndOf="parent"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="parent" />-->
|
||||
|
||||
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/iv_call_videocam_off"-->
|
||||
<!-- android:layout_width="32dp"-->
|
||||
<!-- android:layout_height="32dp"-->
|
||||
<!-- android:clickable="true"-->
|
||||
<!-- android:focusable="true"-->
|
||||
<!-- android:src="@drawable/ic_call_videocam_off_default"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||
<!-- app:layout_constraintEnd_toEndOf="parent"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="parent"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="parent" />-->
|
||||
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/iv_call_mute"-->
|
||||
<!-- android:layout_width="32dp"-->
|
||||
<!-- android:layout_height="32dp"-->
|
||||
<!-- android:layout_marginEnd="32dp"-->
|
||||
<!-- android:clickable="true"-->
|
||||
<!-- android:focusable="true"-->
|
||||
<!-- android:src="@drawable/ic_call_mute_default"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||
<!-- app:layout_constraintEnd_toEndOf="parent"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="parent" />-->
|
||||
|
||||
</LinearLayout>
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<item
|
||||
android:id="@+id/video_call"
|
||||
android:icon="@drawable/ic_videocam"
|
||||
android:icon="@drawable/ic_video"
|
||||
android:title="@string/action_video_call"
|
||||
android:visible="false"
|
||||
app:iconTint="@color/riotx_accent"
|
||||
|
|
Loading…
Reference in a new issue