WIP | Avoid re-negociation pre-agree-upon signaling/negotiation.

This commit is contained in:
Valere 2020-06-11 11:33:02 +02:00
parent 435a6b2f1a
commit 9006acb66a
39 changed files with 1705 additions and 634 deletions

View file

@ -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
}
}

View file

@ -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

View file

@ -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?
}

View file

@ -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,
}

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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>)
// }
// }

View file

@ -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.
*/

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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)

View file

@ -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>

View file

@ -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 -->

View file

@ -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,

View file

@ -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()
}
}

View file

@ -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")
}

View file

@ -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)
}
}

View file

@ -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
)
}
}
}

View file

@ -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")
}

View file

@ -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)
// }
}

View file

@ -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)
// }
// }
// }
//}

View file

@ -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,

View file

@ -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
}
}

View file

@ -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)

View file

@ -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

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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"

View 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>

View file

@ -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"