mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-21 13:48:46 +03:00
Merge pull request #2480 from vector-im/feature/fga/voip_v1_start
VoIP v1 implementation
This commit is contained in:
commit
1b210d42ed
202 changed files with 7347 additions and 3206 deletions
CHANGES.md
gradle/wrapper
matrix-sdk-android
build.gradle
src/main/java/org/matrix/android/sdk
api
MatrixConfiguration.kt
session
Session.kt
call
events/model
room
RoomDirectoryService.kt
model/call
CallAnswerContent.ktCallCandidate.ktCallCandidatesContent.ktCallCapabilities.ktCallHangupContent.ktCallInviteContent.ktCallNegotiateContent.ktCallRejectContent.ktCallReplacesContent.ktCallSelectAnswerContent.ktCallSignallingContent.ktSdpType.kt
summary
timeline
thirdparty
internal
tools/check
vector
build.gradle
src/main
AndroidManifest.xml
assets
java/im/vector/app
VectorApplication.kt
core
di
error
extensions
platform
services
ui/views
utils
features/call
CallAudioManager.ktCallControlsBottomSheet.ktCallControlsView.ktDialerChoiceBottomSheet.ktSharedActiveCallViewModel.ktSharedKnownCallsViewModel.ktVectorCallActivity.ktVectorCallViewActions.ktVectorCallViewEvents.ktVectorCallViewModel.ktVectorCallViewState.ktWebRtcPeerConnectionManager.kt
audio
API21AudioDeviceDetector.ktAPI23AudioDeviceDetector.ktCallAudioManager.ktDefaultAudioDeviceRouter.kt
dialpad
service
telecom
transfer
CallTransferAction.ktCallTransferActivity.ktCallTransferPagerAdapter.ktCallTransferViewEvents.ktCallTransferViewModel.ktCallTransferViewState.kt
utils
webrtc
10
CHANGES.md
10
CHANGES.md
|
@ -2,19 +2,19 @@ Changes in Element 1.0.18 (2020-XX-XX)
|
||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
Features ✨:
|
Features ✨:
|
||||||
-
|
- VoIP : support for VoIP V1 protocol, transfer call and dial-pad
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
-
|
- VoIP : new tiles in timeline
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
-
|
- VoIP : fix audio devices output
|
||||||
|
|
||||||
Translations 🗣:
|
Translations 🗣:
|
||||||
-
|
-
|
||||||
|
|
||||||
SDK API changes ⚠️:
|
SDK API changes ⚠️:
|
||||||
-
|
-
|
||||||
|
|
||||||
Build 🧱:
|
Build 🧱:
|
||||||
-
|
-
|
||||||
|
|
1
gradle/wrapper/gradle-wrapper.properties
vendored
1
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,3 +1,4 @@
|
||||||
|
#Fri Jan 29 18:05:42 CET 2021
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=1433372d903ffba27496f8d5af24265310d2da0d78bf6b4e5138831d4fe066e9
|
distributionSha256Sum=1433372d903ffba27496f8d5af24265310d2da0d78bf6b4e5138831d4fe066e9
|
||||||
|
|
|
@ -168,12 +168,6 @@ dependencies {
|
||||||
// Phone number https://github.com/google/libphonenumber
|
// Phone number https://github.com/google/libphonenumber
|
||||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
|
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
|
||||||
|
|
||||||
// Web RTC
|
|
||||||
// org.webrtc:google-webrtc is for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/
|
|
||||||
// implementation 'org.webrtc:google-webrtc:1.0.+'
|
|
||||||
// Use the same WebRTC library than the one used by Jitsi library
|
|
||||||
implementation('com.facebook.react:react-native-webrtc:1.84.0-jitsi-5112273@aar')
|
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13'
|
||||||
testImplementation 'org.robolectric:robolectric:4.3'
|
testImplementation 'org.robolectric:robolectric:4.3'
|
||||||
//testImplementation 'org.robolectric:shadows-support-v4:3.0'
|
//testImplementation 'org.robolectric:shadows-support-v4:3.0'
|
||||||
|
|
|
@ -35,7 +35,11 @@ data class MatrixConfiguration(
|
||||||
* Optional proxy to connect to the matrix servers
|
* Optional proxy to connect to the matrix servers
|
||||||
* You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port)
|
* You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port)
|
||||||
*/
|
*/
|
||||||
val proxy: Proxy? = null
|
val proxy: Proxy? = null,
|
||||||
|
/**
|
||||||
|
* True to advertise support for call transfers to other parties on Matrix calls.
|
||||||
|
*/
|
||||||
|
val supportsCallTransfer: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -48,6 +48,7 @@ import org.matrix.android.sdk.api.session.signout.SignOutService
|
||||||
import org.matrix.android.sdk.api.session.sync.FilterService
|
import org.matrix.android.sdk.api.session.sync.FilterService
|
||||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||||
import org.matrix.android.sdk.api.session.terms.TermsService
|
import org.matrix.android.sdk.api.session.terms.TermsService
|
||||||
|
import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService
|
||||||
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
|
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
|
||||||
import org.matrix.android.sdk.api.session.user.UserService
|
import org.matrix.android.sdk.api.session.user.UserService
|
||||||
import org.matrix.android.sdk.api.session.widgets.WidgetService
|
import org.matrix.android.sdk.api.session.widgets.WidgetService
|
||||||
|
@ -212,6 +213,11 @@ interface Session :
|
||||||
*/
|
*/
|
||||||
fun searchService(): SearchService
|
fun searchService(): SearchService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the third party service associated with the session
|
||||||
|
*/
|
||||||
|
fun thirdPartyService(): ThirdPartyService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a listener to the session.
|
* Add a listener to the session.
|
||||||
* @param listener the listener to add.
|
* @param listener the listener to add.
|
||||||
|
|
|
@ -20,8 +20,11 @@ import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
|
||||||
|
|
||||||
interface CallsListener {
|
interface CallListener {
|
||||||
/**
|
/**
|
||||||
* Called when there is an incoming call within the room.
|
* Called when there is an incoming call within the room.
|
||||||
*/
|
*/
|
||||||
|
@ -39,5 +42,23 @@ interface CallsListener {
|
||||||
*/
|
*/
|
||||||
fun onCallHangupReceived(callHangupContent: CallHangupContent)
|
fun onCallHangupReceived(callHangupContent: CallHangupContent)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a called has been rejected
|
||||||
|
*/
|
||||||
|
fun onCallRejectReceived(callRejectContent: CallRejectContent)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an answer has been selected
|
||||||
|
*/
|
||||||
|
fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a negotiation is sent
|
||||||
|
*/
|
||||||
|
fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the call has been managed by an other session
|
||||||
|
*/
|
||||||
fun onCallManagedByOtherSession(callId: String)
|
fun onCallManagedByOtherSession(callId: String)
|
||||||
}
|
}
|
|
@ -28,9 +28,9 @@ interface CallSignalingService {
|
||||||
*/
|
*/
|
||||||
fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall
|
fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall
|
||||||
|
|
||||||
fun addCallListener(listener: CallsListener)
|
fun addCallListener(listener: CallListener)
|
||||||
|
|
||||||
fun removeCallListener(listener: CallsListener)
|
fun removeCallListener(listener: CallListener)
|
||||||
|
|
||||||
fun getCallWithId(callId: String): MxCall?
|
fun getCallWithId(callId: String): MxCall?
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,16 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.api.session.call
|
package org.matrix.android.sdk.api.session.call
|
||||||
|
|
||||||
import org.webrtc.PeerConnection
|
|
||||||
|
|
||||||
sealed class CallState {
|
sealed class CallState {
|
||||||
|
|
||||||
/** Idle, setting up objects */
|
/** Idle, setting up objects */
|
||||||
object Idle : CallState()
|
object Idle : CallState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateOffer. Intermediate state between Idle and Dialing.
|
||||||
|
*/
|
||||||
|
object CreateOffer: CallState()
|
||||||
|
|
||||||
/** Dialing. Outgoing call is signaling the remote peer */
|
/** Dialing. Outgoing call is signaling the remote peer */
|
||||||
object Dialing : CallState()
|
object Dialing : CallState()
|
||||||
|
|
||||||
|
@ -36,8 +39,8 @@ sealed class CallState {
|
||||||
* Connected. Incoming/Outgoing call, ice layer connecting or connected
|
* Connected. Incoming/Outgoing call, ice layer connecting or connected
|
||||||
* Notice that the PeerState failed is not always final, if you switch network, new ice candidtates
|
* Notice that the PeerState failed is not always final, if you switch network, new ice candidtates
|
||||||
* could be exchanged, and the connection could go back to connected
|
* could be exchanged, and the connection could go back to connected
|
||||||
*/
|
* */
|
||||||
data class Connected(val iceConnectionState: PeerConnection.PeerConnectionState) : CallState()
|
data class Connected(val iceConnectionState: MxPeerConnectionState) : CallState()
|
||||||
|
|
||||||
/** Terminated. Incoming/Outgoing call, the call is terminated */
|
/** Terminated. Incoming/Outgoing call, the call is terminated */
|
||||||
object Terminated : CallState()
|
object Terminated : CallState()
|
||||||
|
|
|
@ -16,14 +16,17 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.api.session.call
|
package org.matrix.android.sdk.api.session.call
|
||||||
|
|
||||||
import org.webrtc.IceCandidate
|
import org.matrix.android.sdk.api.session.room.model.call.CallCandidate
|
||||||
import org.webrtc.SessionDescription
|
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.SdpType
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
|
||||||
interface MxCallDetail {
|
interface MxCallDetail {
|
||||||
val callId: String
|
val callId: String
|
||||||
val isOutgoing: Boolean
|
val isOutgoing: Boolean
|
||||||
val roomId: String
|
val roomId: String
|
||||||
val otherUserId: String
|
val opponentUserId: String
|
||||||
val isVideoCall: Boolean
|
val isVideoCall: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,40 +35,64 @@ interface MxCallDetail {
|
||||||
*/
|
*/
|
||||||
interface MxCall : MxCallDetail {
|
interface MxCall : MxCallDetail {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val VOIP_PROTO_VERSION = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
val ourPartyId: String
|
||||||
|
var opponentPartyId: Optional<String>?
|
||||||
|
var opponentVersion: Int
|
||||||
|
|
||||||
|
var capabilities: CallCapabilities?
|
||||||
|
|
||||||
var state: CallState
|
var state: CallState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pick Up the incoming call
|
* Pick Up the incoming call
|
||||||
* It has no effect on outgoing call
|
* It has no effect on outgoing call
|
||||||
*/
|
*/
|
||||||
fun accept(sdp: SessionDescription)
|
fun accept(sdpString: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SDP negotiation for media pause, hold/resume, ICE restarts and voice/video call up/downgrading
|
||||||
|
*/
|
||||||
|
fun negotiate(sdpString: String, type: SdpType)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This has to be sent by the caller's client once it has chosen an answer.
|
||||||
|
*/
|
||||||
|
fun selectAnswer()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reject an incoming call
|
* Reject an incoming call
|
||||||
* It's an alias to hangUp
|
|
||||||
*/
|
*/
|
||||||
fun reject() = hangUp()
|
fun reject()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* End the call
|
* End the call
|
||||||
*/
|
*/
|
||||||
fun hangUp()
|
fun hangUp(reason: CallHangupContent.Reason? = null)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a call
|
* Start a call
|
||||||
* Send offer SDP to the other participant.
|
* Send offer SDP to the other participant.
|
||||||
*/
|
*/
|
||||||
fun offerSdp(sdp: SessionDescription)
|
fun offerSdp(sdpString: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Ice candidate to the other participant.
|
* Send Call candidate to the other participant.
|
||||||
*/
|
*/
|
||||||
fun sendLocalIceCandidates(candidates: List<IceCandidate>)
|
fun sendLocalCallCandidates(candidates: List<CallCandidate>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send removed ICE candidates to the other participant.
|
* Send removed ICE candidates to the other participant.
|
||||||
*/
|
*/
|
||||||
fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>)
|
fun sendLocalIceCandidateRemovals(candidates: List<CallCandidate>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a m.call.replaces event to initiate call transfer.
|
||||||
|
*/
|
||||||
|
suspend fun transfer(targetUserId: String, targetRoomId: String?)
|
||||||
|
|
||||||
fun addListener(listener: StateListener)
|
fun addListener(listener: StateListener)
|
||||||
fun removeListener(listener: StateListener)
|
fun removeListener(listener: StateListener)
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.session.call;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a copy of https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState
|
||||||
|
* to avoid having the dependency over WebRtc library on sdk.
|
||||||
|
*/
|
||||||
|
public enum MxPeerConnectionState {
|
||||||
|
NEW,
|
||||||
|
CONNECTING,
|
||||||
|
CONNECTED,
|
||||||
|
DISCONNECTED,
|
||||||
|
FAILED,
|
||||||
|
CLOSED
|
||||||
|
}
|
|
@ -68,7 +68,12 @@ object EventType {
|
||||||
const val CALL_INVITE = "m.call.invite"
|
const val CALL_INVITE = "m.call.invite"
|
||||||
const val CALL_CANDIDATES = "m.call.candidates"
|
const val CALL_CANDIDATES = "m.call.candidates"
|
||||||
const val CALL_ANSWER = "m.call.answer"
|
const val CALL_ANSWER = "m.call.answer"
|
||||||
|
const val CALL_SELECT_ANSWER = "m.call.select_answer"
|
||||||
|
const val CALL_NEGOTIATE = "m.call.negotiate"
|
||||||
|
const val CALL_REJECT = "m.call.reject"
|
||||||
const val CALL_HANGUP = "m.call.hangup"
|
const val CALL_HANGUP = "m.call.hangup"
|
||||||
|
// This type is not processed by the client, just sent to the server
|
||||||
|
const val CALL_REPLACES = "m.call.replaces"
|
||||||
|
|
||||||
// Key share events
|
// Key share events
|
||||||
const val ROOM_KEY_REQUEST = "m.room_key_request"
|
const val ROOM_KEY_REQUEST = "m.room_key_request"
|
||||||
|
@ -98,5 +103,9 @@ object EventType {
|
||||||
|| type == CALL_CANDIDATES
|
|| type == CALL_CANDIDATES
|
||||||
|| type == CALL_ANSWER
|
|| type == CALL_ANSWER
|
||||||
|| type == CALL_HANGUP
|
|| type == CALL_HANGUP
|
||||||
|
|| type == CALL_SELECT_ANSWER
|
||||||
|
|| type == CALL_NEGOTIATE
|
||||||
|
|| type == CALL_REJECT
|
||||||
|
|| type == CALL_REPLACES
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
||||||
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
|
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
|
||||||
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse
|
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse
|
||||||
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
|
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,12 +34,6 @@ interface RoomDirectoryService {
|
||||||
publicRoomsParams: PublicRoomsParams,
|
publicRoomsParams: PublicRoomsParams,
|
||||||
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
|
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the overall metadata about protocols supported by the homeserver.
|
|
||||||
* Includes both the available protocols and all fields required for queries against each protocol.
|
|
||||||
*/
|
|
||||||
fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the visibility of a room in the directory
|
* Get the visibility of a room in the directory
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -27,16 +27,24 @@ data class CallAnswerContent(
|
||||||
/**
|
/**
|
||||||
* Required. The ID of the call this event relates to.
|
* Required. The ID of the call this event relates to.
|
||||||
*/
|
*/
|
||||||
@Json(name = "call_id") val callId: String,
|
@Json(name = "call_id") override val callId: String,
|
||||||
|
/**
|
||||||
|
* Required. ID to let user identify remote echo of their own events
|
||||||
|
*/
|
||||||
|
@Json(name = "party_id") override val partyId: String? = null,
|
||||||
/**
|
/**
|
||||||
* Required. The session description object
|
* Required. The session description object
|
||||||
*/
|
*/
|
||||||
@Json(name = "answer") val answer: Answer,
|
@Json(name = "answer") val answer: Answer,
|
||||||
/**
|
/**
|
||||||
* Required. The version of the VoIP specification this messages adheres to. This specification is version 0.
|
* Required. The version of the VoIP specification this messages adheres to.
|
||||||
*/
|
*/
|
||||||
@Json(name = "version") val version: Int = 0
|
@Json(name = "version") override val version: String?,
|
||||||
) {
|
/**
|
||||||
|
* Capability advertisement.
|
||||||
|
*/
|
||||||
|
@Json(name = "capabilities") val capabilities: CallCapabilities? = null
|
||||||
|
): CallSignallingContent {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class Answer(
|
data class Answer(
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.session.room.model.call
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class CallCandidate(
|
||||||
|
/**
|
||||||
|
* Required. The SDP media type this candidate is intended for.
|
||||||
|
*/
|
||||||
|
@Json(name = "sdpMid") val sdpMid: String? = null,
|
||||||
|
/**
|
||||||
|
* Required. The index of the SDP 'm' line this candidate is intended for.
|
||||||
|
*/
|
||||||
|
@Json(name = "sdpMLineIndex") val sdpMLineIndex: Int = 0,
|
||||||
|
/**
|
||||||
|
* Required. The SDP 'a' line of the candidate.
|
||||||
|
*/
|
||||||
|
@Json(name = "candidate") val candidate: String? = null
|
||||||
|
)
|
|
@ -28,30 +28,17 @@ data class CallCandidatesContent(
|
||||||
/**
|
/**
|
||||||
* Required. The ID of the call this event relates to.
|
* Required. The ID of the call this event relates to.
|
||||||
*/
|
*/
|
||||||
@Json(name = "call_id") val callId: String,
|
@Json(name = "call_id") override val callId: String,
|
||||||
|
/**
|
||||||
|
* Required. ID to let user identify remote echo of their own events
|
||||||
|
*/
|
||||||
|
@Json(name = "party_id") override val partyId: String? = null,
|
||||||
/**
|
/**
|
||||||
* Required. Array of objects describing the candidates.
|
* Required. Array of objects describing the candidates.
|
||||||
*/
|
*/
|
||||||
@Json(name = "candidates") val candidates: List<Candidate> = emptyList(),
|
@Json(name = "candidates") val candidates: List<CallCandidate> = emptyList(),
|
||||||
/**
|
/**
|
||||||
* Required. The version of the VoIP specification this messages adheres to. This specification is version 0.
|
* Required. The version of the VoIP specification this messages adheres to.
|
||||||
*/
|
*/
|
||||||
@Json(name = "version") val version: Int = 0
|
@Json(name = "version") override val version: String?
|
||||||
) {
|
): CallSignallingContent
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class Candidate(
|
|
||||||
/**
|
|
||||||
* Required. The SDP media type this candidate is intended for.
|
|
||||||
*/
|
|
||||||
@Json(name = "sdpMid") val sdpMid: String,
|
|
||||||
/**
|
|
||||||
* Required. The index of the SDP 'm' line this candidate is intended for.
|
|
||||||
*/
|
|
||||||
@Json(name = "sdpMLineIndex") val sdpMLineIndex: Int,
|
|
||||||
/**
|
|
||||||
* Required. The SDP 'a' line of the candidate.
|
|
||||||
*/
|
|
||||||
@Json(name = "candidate") val candidate: String
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.session.room.model.call
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class CallCapabilities(
|
||||||
|
/**
|
||||||
|
* If set to true, states that the sender of the event supports the m.call.replaces event and therefore supports
|
||||||
|
* being transferred to another destination
|
||||||
|
*/
|
||||||
|
@Json(name = "m.call.transferee") val transferee: Boolean? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
fun CallCapabilities?.supportCallTransfer() = this?.transferee.orFalse()
|
|
@ -28,24 +28,41 @@ data class CallHangupContent(
|
||||||
/**
|
/**
|
||||||
* Required. The ID of the call this event relates to.
|
* Required. The ID of the call this event relates to.
|
||||||
*/
|
*/
|
||||||
@Json(name = "call_id") val callId: String,
|
@Json(name = "call_id") override val callId: String,
|
||||||
/**
|
/**
|
||||||
* Required. The version of the VoIP specification this message adheres to. This specification is version 0.
|
* Required. ID to let user identify remote echo of their own events
|
||||||
*/
|
*/
|
||||||
@Json(name = "version") val version: Int = 0,
|
@Json(name = "party_id") override val partyId: String? = null,
|
||||||
|
/**
|
||||||
|
* Required. The version of the VoIP specification this message adheres to.
|
||||||
|
*/
|
||||||
|
@Json(name = "version") override val version: String?,
|
||||||
/**
|
/**
|
||||||
* Optional error reason for the hangup. This should not be provided when the user naturally ends or rejects the call.
|
* Optional error reason for the hangup. This should not be provided when the user naturally ends or rejects the call.
|
||||||
* When there was an error in the call negotiation, this should be `ice_failed` for when ICE negotiation fails
|
* When there was an error in the call negotiation, this should be `ice_failed` for when ICE negotiation fails
|
||||||
* or `invite_timeout` for when the other party did not answer in time. One of: ["ice_failed", "invite_timeout"]
|
* or `invite_timeout` for when the other party did not answer in time.
|
||||||
|
* One of: ["ice_failed", "invite_timeout"]
|
||||||
*/
|
*/
|
||||||
@Json(name = "reason") val reason: Reason? = null
|
@Json(name = "reason") val reason: Reason? = null
|
||||||
) {
|
) : CallSignallingContent {
|
||||||
@JsonClass(generateAdapter = false)
|
@JsonClass(generateAdapter = false)
|
||||||
enum class Reason {
|
enum class Reason {
|
||||||
@Json(name = "ice_failed")
|
@Json(name = "ice_failed")
|
||||||
ICE_FAILED,
|
ICE_FAILED,
|
||||||
|
|
||||||
|
@Json(name = "ice_timeout")
|
||||||
|
ICE_TIMEOUT,
|
||||||
|
|
||||||
|
@Json(name = "user_hangup")
|
||||||
|
USER_HANGUP,
|
||||||
|
|
||||||
|
@Json(name = "user_media_failed")
|
||||||
|
USER_MEDIA_FAILED,
|
||||||
|
|
||||||
@Json(name = "invite_timeout")
|
@Json(name = "invite_timeout")
|
||||||
INVITE_TIMEOUT
|
INVITE_TIMEOUT,
|
||||||
|
|
||||||
|
@Json(name = "unknown_error")
|
||||||
|
UNKWOWN_ERROR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,22 +27,35 @@ data class CallInviteContent(
|
||||||
/**
|
/**
|
||||||
* Required. A unique identifier for the call.
|
* Required. A unique identifier for the call.
|
||||||
*/
|
*/
|
||||||
@Json(name = "call_id") val callId: String?,
|
@Json(name = "call_id") override val callId: String?,
|
||||||
|
/**
|
||||||
|
* Required. ID to let user identify remote echo of their own events
|
||||||
|
*/
|
||||||
|
@Json(name = "party_id") override val partyId: String? = null,
|
||||||
/**
|
/**
|
||||||
* Required. The session description object
|
* Required. The session description object
|
||||||
*/
|
*/
|
||||||
@Json(name = "offer") val offer: Offer?,
|
@Json(name = "offer") val offer: Offer?,
|
||||||
/**
|
/**
|
||||||
* Required. The version of the VoIP specification this message adheres to. This specification is version 0.
|
* Required. The version of the VoIP specification this message adheres to.
|
||||||
*/
|
*/
|
||||||
@Json(name = "version") val version: Int? = 0,
|
@Json(name = "version") override val version: String?,
|
||||||
/**
|
/**
|
||||||
* Required. The time in milliseconds that the invite is valid for.
|
* Required. The time in milliseconds that the invite is valid for.
|
||||||
* Once the invite age exceeds this value, clients should discard it.
|
* Once the invite age exceeds this value, clients should discard it.
|
||||||
* They should also no longer show the call as awaiting an answer in the UI.
|
* They should also no longer show the call as awaiting an answer in the UI.
|
||||||
*/
|
*/
|
||||||
@Json(name = "lifetime") val lifetime: Int?
|
@Json(name = "lifetime") val lifetime: Int?,
|
||||||
) {
|
/**
|
||||||
|
* The field should be added for all invites where the target is a specific user
|
||||||
|
*/
|
||||||
|
@Json(name = "invitee") val invitee: String? = null,
|
||||||
|
/**
|
||||||
|
* Capability advertisement.
|
||||||
|
*/
|
||||||
|
@Json(name = "capabilities") val capabilities: CallCapabilities? = null
|
||||||
|
|
||||||
|
): CallSignallingContent {
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class Offer(
|
data class Offer(
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.session.room.model.call
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This introduces SDP negotiation semantics for media pause, hold/resume, ICE restarts and voice/video call up/downgrading.
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class CallNegotiateContent(
|
||||||
|
/**
|
||||||
|
* Required. The ID of the call this event relates to.
|
||||||
|
*/
|
||||||
|
@Json(name = "call_id") override val callId: String,
|
||||||
|
/**
|
||||||
|
* Required. ID to let user identify remote echo of their own events
|
||||||
|
*/
|
||||||
|
@Json(name = "party_id") override val partyId: String? = null,
|
||||||
|
/**
|
||||||
|
* Required. The time in milliseconds that the negotiation is valid for. Once exceeded the sender
|
||||||
|
* of the negotiate event should consider the negotiation failed (timed out) and the recipient should ignore it.
|
||||||
|
**/
|
||||||
|
@Json(name = "lifetime") val lifetime: Int?,
|
||||||
|
/**
|
||||||
|
* Required. The session description object
|
||||||
|
*/
|
||||||
|
@Json(name = "description") val description: Description? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. The version of the VoIP specification this message adheres to.
|
||||||
|
*/
|
||||||
|
@Json(name = "version") override val version: String?
|
||||||
|
|
||||||
|
): CallSignallingContent {
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class Description(
|
||||||
|
/**
|
||||||
|
* Required. The type of session description.
|
||||||
|
*/
|
||||||
|
@Json(name = "type") val type: SdpType?,
|
||||||
|
/**
|
||||||
|
* Required. The SDP text of the session description.
|
||||||
|
*/
|
||||||
|
@Json(name = "sdp") val sdp: String?
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.session.room.model.call
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sent by either party to signal their termination of the call. This can be sent either once
|
||||||
|
* the call has been established or before to abort the call.
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class CallRejectContent(
|
||||||
|
/**
|
||||||
|
* Required. The ID of the call this event relates to.
|
||||||
|
*/
|
||||||
|
@Json(name = "call_id") override val callId: String,
|
||||||
|
/**
|
||||||
|
* Required. ID to let user identify remote echo of their own events
|
||||||
|
*/
|
||||||
|
@Json(name = "party_id") override val partyId: String? = null,
|
||||||
|
/**
|
||||||
|
* Required. The version of the VoIP specification this message adheres to.
|
||||||
|
*/
|
||||||
|
@Json(name = "version") override val version: String?
|
||||||
|
) : CallSignallingContent
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.session.room.model.call
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is sent to signal the intent of a participant in a call to replace the call with another,
|
||||||
|
* such that the other participant ends up in a call with a new user.
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class CallReplacesContent(
|
||||||
|
/**
|
||||||
|
* Required. The ID of the call this event relates to.
|
||||||
|
*/
|
||||||
|
@Json(name = "call_id") override val callId: String,
|
||||||
|
/**
|
||||||
|
* Required. ID to let user identify remote echo of their own events
|
||||||
|
*/
|
||||||
|
@Json(name = "party_id") override val partyId: String? = null,
|
||||||
|
/**
|
||||||
|
* An identifier for the call replacement itself, generated by the transferor.
|
||||||
|
*/
|
||||||
|
@Json(name = "replacement_id") val replacementId: String? = null,
|
||||||
|
/**
|
||||||
|
* Optional. If specified, the transferee client waits for an invite to this room and joins it
|
||||||
|
* (possibly waiting for user confirmation) and then continues the transfer in this room.
|
||||||
|
* If absent, the transferee contacts the Matrix User ID given in the target_user field in a room of its choosing.
|
||||||
|
*/
|
||||||
|
@Json(name = "target_room") val targerRoomId: String? = null,
|
||||||
|
/**
|
||||||
|
* An object giving information about the transfer target
|
||||||
|
*/
|
||||||
|
@Json(name = "target_user") val targetUser: TargetUser? = null,
|
||||||
|
/**
|
||||||
|
* If specified, gives the call ID for the transferee's client to use when placing the replacement call.
|
||||||
|
* Mutually exclusive with await_call
|
||||||
|
*/
|
||||||
|
@Json(name = "create_call") val createCall: String? = null,
|
||||||
|
/**
|
||||||
|
* If specified, gives the call ID that the transferee's client should wait for.
|
||||||
|
* Mutually exclusive with create_call.
|
||||||
|
*/
|
||||||
|
@Json(name = "await_call") val awaitCall: String? = null,
|
||||||
|
/**
|
||||||
|
* Required. The version of the VoIP specification this messages adheres to.
|
||||||
|
*/
|
||||||
|
@Json(name = "version") override val version: String?
|
||||||
|
): CallSignallingContent {
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class TargetUser(
|
||||||
|
/**
|
||||||
|
* Required. The matrix user ID of the transfer target
|
||||||
|
*/
|
||||||
|
@Json(name = "id") val id: String,
|
||||||
|
/**
|
||||||
|
* Optional. The display name of the transfer target.
|
||||||
|
*/
|
||||||
|
@Json(name = "display_name") val displayName: String?,
|
||||||
|
/**
|
||||||
|
* Optional. The avatar URL of the transfer target.
|
||||||
|
*/
|
||||||
|
@Json(name = "avatar_url") val avatarUrl: String?
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.session.room.model.call
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is sent by the callee when they wish to answer the call.
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class CallSelectAnswerContent(
|
||||||
|
/**
|
||||||
|
* Required. The ID of the call this event relates to.
|
||||||
|
*/
|
||||||
|
@Json(name = "call_id") override val callId: String,
|
||||||
|
/**
|
||||||
|
* Required. ID to let user identify remote echo of their own events
|
||||||
|
*/
|
||||||
|
@Json(name = "party_id") override val partyId: String? = null,
|
||||||
|
/**
|
||||||
|
* Required. Indicates the answer user has chosen.
|
||||||
|
*/
|
||||||
|
@Json(name = "selected_party_id") val selectedPartyId: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. The version of the VoIP specification this message adheres to.
|
||||||
|
*/
|
||||||
|
@Json(name = "version") override val version: String?
|
||||||
|
): CallSignallingContent
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.session.room.model.call
|
||||||
|
|
||||||
|
interface CallSignallingContent {
|
||||||
|
/**
|
||||||
|
* Required. A unique identifier for the call.
|
||||||
|
*/
|
||||||
|
val callId: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. ID to let user identify remote echo of their own events
|
||||||
|
*/
|
||||||
|
val partyId: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. The version of the VoIP specification this message adheres to. This specification is version 0.
|
||||||
|
*/
|
||||||
|
val version: String?
|
||||||
|
}
|
|
@ -25,5 +25,5 @@ enum class SdpType {
|
||||||
OFFER,
|
OFFER,
|
||||||
|
|
||||||
@Json(name = "answer")
|
@Json(name = "answer")
|
||||||
ANSWER
|
ANSWER;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,15 @@ import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
|
||||||
object RoomSummaryConstants {
|
object RoomSummaryConstants {
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
val PREVIEWABLE_TYPES = listOf(
|
val PREVIEWABLE_TYPES = listOf(
|
||||||
// TODO filter message type (KEY_VERIFICATION_READY, etc.)
|
// TODO filter message type (KEY_VERIFICATION_READY, etc.)
|
||||||
EventType.MESSAGE,
|
EventType.MESSAGE,
|
||||||
EventType.CALL_INVITE,
|
EventType.CALL_INVITE,
|
||||||
EventType.CALL_HANGUP,
|
EventType.CALL_HANGUP,
|
||||||
|
EventType.CALL_REJECT,
|
||||||
EventType.CALL_ANSWER,
|
EventType.CALL_ANSWER,
|
||||||
EventType.ENCRYPTED,
|
EventType.ENCRYPTED,
|
||||||
EventType.STICKER,
|
EventType.STICKER,
|
||||||
|
|
|
@ -52,6 +52,8 @@ data class TimelineEvent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val roomId = root.roomId ?: ""
|
||||||
|
|
||||||
val metadata = HashMap<String, Any>()
|
val metadata = HashMap<String, Any>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 The Matrix.org Foundation C.I.C
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.session.thirdparty
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||||
|
import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-thirdparty-protocols
|
||||||
|
*/
|
||||||
|
interface ThirdPartyService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the overall metadata about protocols supported by the homeserver.
|
||||||
|
* Includes both the available protocols and all fields required for queries against each protocol.
|
||||||
|
*/
|
||||||
|
suspend fun getThirdPartyProtocols(): Map<String, ThirdPartyProtocol>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a Matrix User ID linked to a user on the third party service, given a set of user parameters.
|
||||||
|
* @param protocol Required. The name of the protocol.
|
||||||
|
* @param fields One or more custom fields that are passed to the AS to help identify the user.
|
||||||
|
*/
|
||||||
|
suspend fun getThirdPartyUser(protocol: String, fields: Map<String, String> = emptyMap()): List<ThirdPartyUser>
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 The Matrix.org Foundation C.I.C
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.session.thirdparty.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ThirdPartyUser(
|
||||||
|
/*
|
||||||
|
Required. A Matrix User ID represting a third party user.
|
||||||
|
*/
|
||||||
|
@Json(name = "userid") val userId: String,
|
||||||
|
/*
|
||||||
|
Required. The protocol ID that the third party location is a part of.
|
||||||
|
*/
|
||||||
|
@Json(name = "protocol") val protocol: String,
|
||||||
|
/*
|
||||||
|
Required. Information used to identify this third party location.
|
||||||
|
*/
|
||||||
|
@Json(name = "fields") val fields: JsonDict
|
||||||
|
)
|
|
@ -71,7 +71,6 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
val domainEvent = event.asDomain()
|
val domainEvent = event.asDomain()
|
||||||
// decryptIfNeeded(domainEvent)
|
|
||||||
processors.filter {
|
processors.filter {
|
||||||
it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
|
it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
|
||||||
}.forEach {
|
}.forEach {
|
||||||
|
@ -83,6 +82,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
|
||||||
.findAll()
|
.findAll()
|
||||||
.deleteAllFromRealm()
|
.deleteAllFromRealm()
|
||||||
}
|
}
|
||||||
|
processors.forEach { it.onPostProcess() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageServi
|
||||||
import org.matrix.android.sdk.api.session.signout.SignOutService
|
import org.matrix.android.sdk.api.session.signout.SignOutService
|
||||||
import org.matrix.android.sdk.api.session.sync.FilterService
|
import org.matrix.android.sdk.api.session.sync.FilterService
|
||||||
import org.matrix.android.sdk.api.session.terms.TermsService
|
import org.matrix.android.sdk.api.session.terms.TermsService
|
||||||
|
import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService
|
||||||
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
|
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
|
||||||
import org.matrix.android.sdk.api.session.user.UserService
|
import org.matrix.android.sdk.api.session.user.UserService
|
||||||
import org.matrix.android.sdk.api.session.widgets.WidgetService
|
import org.matrix.android.sdk.api.session.widgets.WidgetService
|
||||||
|
@ -114,6 +115,7 @@ internal class DefaultSession @Inject constructor(
|
||||||
private val accountService: Lazy<AccountService>,
|
private val accountService: Lazy<AccountService>,
|
||||||
private val defaultIdentityService: DefaultIdentityService,
|
private val defaultIdentityService: DefaultIdentityService,
|
||||||
private val integrationManagerService: IntegrationManagerService,
|
private val integrationManagerService: IntegrationManagerService,
|
||||||
|
private val thirdPartyService: Lazy<ThirdPartyService>,
|
||||||
private val callSignalingService: Lazy<CallSignalingService>,
|
private val callSignalingService: Lazy<CallSignalingService>,
|
||||||
@UnauthenticatedWithCertificate
|
@UnauthenticatedWithCertificate
|
||||||
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>,
|
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>,
|
||||||
|
@ -258,6 +260,8 @@ internal class DefaultSession @Inject constructor(
|
||||||
|
|
||||||
override fun searchService(): SearchService = searchService.get()
|
override fun searchService(): SearchService = searchService.get()
|
||||||
|
|
||||||
|
override fun thirdPartyService(): ThirdPartyService = thirdPartyService.get()
|
||||||
|
|
||||||
override fun getOkHttpClient(): OkHttpClient {
|
override fun getOkHttpClient(): OkHttpClient {
|
||||||
return unauthenticatedWithCertificateOkHttpClient.get()
|
return unauthenticatedWithCertificateOkHttpClient.get()
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,4 +25,12 @@ internal interface EventInsertLiveProcessor {
|
||||||
fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean
|
fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean
|
||||||
|
|
||||||
suspend fun process(realm: Realm, event: Event)
|
suspend fun process(realm: Realm, event: Event)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after transaction.
|
||||||
|
* Maybe you prefer to process the events outside of the realm transaction.
|
||||||
|
*/
|
||||||
|
suspend fun onPostProcess() {
|
||||||
|
// Noop by default
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ import org.matrix.android.sdk.internal.session.sync.SyncTask
|
||||||
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
|
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
|
||||||
import org.matrix.android.sdk.internal.session.sync.job.SyncWorker
|
import org.matrix.android.sdk.internal.session.sync.job.SyncWorker
|
||||||
import org.matrix.android.sdk.internal.session.terms.TermsModule
|
import org.matrix.android.sdk.internal.session.terms.TermsModule
|
||||||
|
import org.matrix.android.sdk.internal.session.thirdparty.ThirdPartyModule
|
||||||
import org.matrix.android.sdk.internal.session.user.UserModule
|
import org.matrix.android.sdk.internal.session.user.UserModule
|
||||||
import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataModule
|
import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataModule
|
||||||
import org.matrix.android.sdk.internal.session.widgets.WidgetModule
|
import org.matrix.android.sdk.internal.session.widgets.WidgetModule
|
||||||
|
@ -87,7 +88,8 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||||
ProfileModule::class,
|
ProfileModule::class,
|
||||||
AccountModule::class,
|
AccountModule::class,
|
||||||
CallModule::class,
|
CallModule::class,
|
||||||
SearchModule::class
|
SearchModule::class,
|
||||||
|
ThirdPartyModule::class
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@SessionScope
|
@SessionScope
|
||||||
|
|
|
@ -16,28 +16,30 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.call
|
package org.matrix.android.sdk.internal.session.call
|
||||||
|
|
||||||
|
import io.realm.Realm
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
||||||
import io.realm.Realm
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class CallEventProcessor @Inject constructor(
|
internal class CallEventProcessor @Inject constructor(private val callSignalingHandler: CallSignalingHandler)
|
||||||
@UserId private val userId: String,
|
: EventInsertLiveProcessor {
|
||||||
private val callService: DefaultCallSignalingService
|
|
||||||
) : EventInsertLiveProcessor {
|
|
||||||
|
|
||||||
private val allowedTypes = listOf(
|
private val allowedTypes = listOf(
|
||||||
EventType.CALL_ANSWER,
|
EventType.CALL_ANSWER,
|
||||||
|
EventType.CALL_SELECT_ANSWER,
|
||||||
|
EventType.CALL_REJECT,
|
||||||
|
EventType.CALL_NEGOTIATE,
|
||||||
EventType.CALL_CANDIDATES,
|
EventType.CALL_CANDIDATES,
|
||||||
EventType.CALL_INVITE,
|
EventType.CALL_INVITE,
|
||||||
EventType.CALL_HANGUP,
|
EventType.CALL_HANGUP,
|
||||||
EventType.ENCRYPTED
|
EventType.ENCRYPTED
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val eventsToPostProcess = mutableListOf<Event>()
|
||||||
|
|
||||||
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
|
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
|
||||||
if (insertType != EventInsertType.INCREMENTAL_SYNC) {
|
if (insertType != EventInsertType.INCREMENTAL_SYNC) {
|
||||||
return false
|
return false
|
||||||
|
@ -46,10 +48,17 @@ internal class CallEventProcessor @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun process(realm: Realm, event: Event) {
|
override suspend fun process(realm: Realm, event: Event) {
|
||||||
update(realm, event)
|
eventsToPostProcess.add(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun update(realm: Realm, event: Event) {
|
override suspend fun onPostProcess() {
|
||||||
|
eventsToPostProcess.forEach {
|
||||||
|
dispatchToCallSignalingHandlerIfNeeded(it)
|
||||||
|
}
|
||||||
|
eventsToPostProcess.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dispatchToCallSignalingHandlerIfNeeded(event: Event) {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
// TODO might check if an invite is not closed (hangup/answsered) in the same event batch?
|
// TODO might check if an invite is not closed (hangup/answsered) in the same event batch?
|
||||||
event.roomId ?: return Unit.also {
|
event.roomId ?: return Unit.also {
|
||||||
|
@ -60,10 +69,6 @@ internal class CallEventProcessor @Inject constructor(
|
||||||
// To old to ring?
|
// To old to ring?
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
event.ageLocalTs
|
callSignalingHandler.onCallEvent(event)
|
||||||
if (EventType.isCallEvent(event.getClearType())) {
|
|
||||||
callService.onCallEvent(event)
|
|
||||||
}
|
|
||||||
Timber.v("$realm : $userId")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.session.call
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
import org.matrix.android.sdk.api.session.call.CallListener
|
||||||
|
import org.matrix.android.sdk.api.session.call.MxCall
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch each method safely to all listeners.
|
||||||
|
*/
|
||||||
|
internal class CallListenersDispatcher(private val listeners: Set<CallListener>) : CallListener {
|
||||||
|
|
||||||
|
override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) = dispatch {
|
||||||
|
it.onCallInviteReceived(mxCall, callInviteContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) = dispatch {
|
||||||
|
it.onCallIceCandidateReceived(mxCall, iceCandidatesContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) = dispatch {
|
||||||
|
it.onCallAnswerReceived(callAnswerContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCallHangupReceived(callHangupContent: CallHangupContent) = dispatch {
|
||||||
|
it.onCallHangupReceived(callHangupContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCallRejectReceived(callRejectContent: CallRejectContent) = dispatch {
|
||||||
|
it.onCallRejectReceived(callRejectContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCallManagedByOtherSession(callId: String) = dispatch {
|
||||||
|
it.onCallManagedByOtherSession(callId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) = dispatch {
|
||||||
|
it.onCallSelectAnswerReceived(callSelectAnswerContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) = dispatch {
|
||||||
|
it.onCallNegotiateReceived(callNegotiateContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dispatch(lambda: (CallListener) -> Unit) {
|
||||||
|
listeners.toList().forEach {
|
||||||
|
tryOrNull {
|
||||||
|
lambda(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.session.call
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.call.CallListener
|
||||||
|
import org.matrix.android.sdk.api.session.call.CallState
|
||||||
|
import org.matrix.android.sdk.api.session.call.MxCall
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallSignallingContent
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SessionScope
|
||||||
|
internal class CallSignalingHandler @Inject constructor(private val activeCallHandler: ActiveCallHandler,
|
||||||
|
private val mxCallFactory: MxCallFactory,
|
||||||
|
@UserId private val userId: String) {
|
||||||
|
|
||||||
|
private val callListeners = mutableSetOf<CallListener>()
|
||||||
|
private val callListenersDispatcher = CallListenersDispatcher(callListeners)
|
||||||
|
|
||||||
|
fun addCallListener(listener: CallListener) {
|
||||||
|
callListeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCallListener(listener: CallListener) {
|
||||||
|
callListeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCallEvent(event: Event) {
|
||||||
|
when (event.getClearType()) {
|
||||||
|
EventType.CALL_ANSWER -> {
|
||||||
|
handleCallAnswerEvent(event)
|
||||||
|
}
|
||||||
|
EventType.CALL_INVITE -> {
|
||||||
|
handleCallInviteEvent(event)
|
||||||
|
}
|
||||||
|
EventType.CALL_HANGUP -> {
|
||||||
|
handleCallHangupEvent(event)
|
||||||
|
}
|
||||||
|
EventType.CALL_REJECT -> {
|
||||||
|
handleCallRejectEvent(event)
|
||||||
|
}
|
||||||
|
EventType.CALL_CANDIDATES -> {
|
||||||
|
handleCallCandidatesEvent(event)
|
||||||
|
}
|
||||||
|
EventType.CALL_SELECT_ANSWER -> {
|
||||||
|
handleCallSelectAnswerEvent(event)
|
||||||
|
}
|
||||||
|
EventType.CALL_NEGOTIATE -> {
|
||||||
|
handleCallNegotiateEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCallNegotiateEvent(event: Event) {
|
||||||
|
val content = event.getClearContent().toModel<CallNegotiateContent>() ?: return
|
||||||
|
val call = content.getCall() ?: return
|
||||||
|
if (call.ourPartyId == content.partyId) {
|
||||||
|
// Ignore remote echo
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callListenersDispatcher.onCallNegotiateReceived(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCallSelectAnswerEvent(event: Event) {
|
||||||
|
val content = event.getClearContent().toModel<CallSelectAnswerContent>() ?: return
|
||||||
|
val call = content.getCall() ?: return
|
||||||
|
if (call.ourPartyId == content.partyId) {
|
||||||
|
// Ignore remote echo
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (call.isOutgoing) {
|
||||||
|
Timber.v("Got selectAnswer for an outbound call: ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val selectedPartyId = content.selectedPartyId
|
||||||
|
if (selectedPartyId == null) {
|
||||||
|
Timber.w("Got nonsensical select_answer with null selected_party_id: ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callListenersDispatcher.onCallSelectAnswerReceived(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCallCandidatesEvent(event: Event) {
|
||||||
|
val content = event.getClearContent().toModel<CallCandidatesContent>() ?: return
|
||||||
|
val call = content.getCall() ?: return
|
||||||
|
if (call.ourPartyId == content.partyId) {
|
||||||
|
// Ignore remote echo
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (call.opponentPartyId != null && !call.partyIdsMatches(content)) {
|
||||||
|
Timber.v("Ignoring candidates from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callListenersDispatcher.onCallIceCandidateReceived(call, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCallRejectEvent(event: Event) {
|
||||||
|
val content = event.getClearContent().toModel<CallRejectContent>() ?: return
|
||||||
|
val call = content.getCall() ?: return
|
||||||
|
if (call.ourPartyId == content.partyId) {
|
||||||
|
// Ignore remote echo
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activeCallHandler.removeCall(content.callId)
|
||||||
|
if (event.senderId == userId) {
|
||||||
|
// discard current call, it's rejected by another of my session
|
||||||
|
callListenersDispatcher.onCallManagedByOtherSession(content.callId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// No need to check party_id for reject because if we'd received either
|
||||||
|
// an answer or reject, we wouldn't be in state InviteSent
|
||||||
|
if (call.state != CallState.Dialing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callListenersDispatcher.onCallRejectReceived(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCallHangupEvent(event: Event) {
|
||||||
|
val content = event.getClearContent().toModel<CallHangupContent>() ?: return
|
||||||
|
val call = content.getCall() ?: return
|
||||||
|
// party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen
|
||||||
|
// a partner yet but we're treating the hangup as a reject as per VoIP v0)
|
||||||
|
if (call.opponentPartyId != null && !call.partyIdsMatches(content)) {
|
||||||
|
Timber.v("Ignoring hangup from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (call.state != CallState.Terminated) {
|
||||||
|
activeCallHandler.removeCall(content.callId)
|
||||||
|
callListenersDispatcher.onCallHangupReceived(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCallInviteEvent(event: Event) {
|
||||||
|
if (event.senderId == userId) {
|
||||||
|
// ignore invites you send
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.roomId == null || event.senderId == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val content = event.getClearContent().toModel<CallInviteContent>() ?: return
|
||||||
|
val incomingCall = mxCallFactory.createIncomingCall(
|
||||||
|
roomId = event.roomId,
|
||||||
|
opponentUserId = event.senderId,
|
||||||
|
content = content
|
||||||
|
) ?: return
|
||||||
|
activeCallHandler.addCall(incomingCall)
|
||||||
|
callListenersDispatcher.onCallInviteReceived(incomingCall, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCallAnswerEvent(event: Event) {
|
||||||
|
val content = event.getClearContent().toModel<CallAnswerContent>() ?: return
|
||||||
|
val call = content.getCall() ?: return
|
||||||
|
if (call.ourPartyId == content.partyId) {
|
||||||
|
// Ignore remote echo
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.senderId == userId) {
|
||||||
|
// discard current call, it's answered by another of my session
|
||||||
|
activeCallHandler.removeCall(call.callId)
|
||||||
|
callListenersDispatcher.onCallManagedByOtherSession(content.callId)
|
||||||
|
} else {
|
||||||
|
if (call.opponentPartyId != null) {
|
||||||
|
Timber.v("Ignoring answer from party ID ${content.partyId} we already have an answer from ${call.opponentPartyId}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
call.apply {
|
||||||
|
opponentPartyId = Optional.from(content.partyId)
|
||||||
|
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
|
||||||
|
capabilities = content.capabilities ?: CallCapabilities()
|
||||||
|
}
|
||||||
|
callListenersDispatcher.onCallAnswerReceived(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MxCall.partyIdsMatches(contentSignallingContent: CallSignallingContent): Boolean {
|
||||||
|
return opponentPartyId?.getOrNull() == contentSignallingContent.partyId
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CallSignallingContent.getCall(): MxCall? {
|
||||||
|
val currentCall = callId?.let {
|
||||||
|
activeCallHandler.getCallWithId(it)
|
||||||
|
}
|
||||||
|
if (currentCall == null) {
|
||||||
|
Timber.v("Call with id $callId is null")
|
||||||
|
}
|
||||||
|
return currentCall
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,106 +16,46 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.call
|
package org.matrix.android.sdk.internal.session.call
|
||||||
|
|
||||||
import android.os.SystemClock
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.session.call.CallListener
|
||||||
import org.matrix.android.sdk.api.session.call.CallSignalingService
|
import org.matrix.android.sdk.api.session.call.CallSignalingService
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
|
||||||
import org.matrix.android.sdk.api.session.call.CallsListener
|
|
||||||
import org.matrix.android.sdk.api.session.call.MxCall
|
import org.matrix.android.sdk.api.session.call.MxCall
|
||||||
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
import org.matrix.android.sdk.api.util.NoOpCancellable
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
import org.matrix.android.sdk.internal.session.call.model.MxCallImpl
|
|
||||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
|
||||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||||
import org.matrix.android.sdk.internal.task.configureWith
|
import org.matrix.android.sdk.internal.task.launchToCallback
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class DefaultCallSignalingService @Inject constructor(
|
internal class DefaultCallSignalingService @Inject constructor(
|
||||||
@UserId
|
private val callSignalingHandler: CallSignalingHandler,
|
||||||
private val userId: String,
|
private val mxCallFactory: MxCallFactory,
|
||||||
private val activeCallHandler: ActiveCallHandler,
|
private val activeCallHandler: ActiveCallHandler,
|
||||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
|
||||||
private val eventSenderProcessor: EventSenderProcessor,
|
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val turnServerTask: GetTurnServerTask
|
private val turnServerDataSource: TurnServerDataSource
|
||||||
) : CallSignalingService {
|
) : CallSignalingService {
|
||||||
|
|
||||||
private val callListeners = mutableSetOf<CallsListener>()
|
|
||||||
|
|
||||||
private val cachedTurnServerResponse = object {
|
|
||||||
// Keep one minute safe to avoid considering the data is valid and then actually it is not when effectively using it.
|
|
||||||
private val MIN_TTL = 60
|
|
||||||
|
|
||||||
private val now = { SystemClock.elapsedRealtime() / 1000 }
|
|
||||||
|
|
||||||
private var expiresAt: Long = 0
|
|
||||||
|
|
||||||
var data: TurnServerResponse? = null
|
|
||||||
get() = if (expiresAt > now()) field else null
|
|
||||||
set(value) {
|
|
||||||
expiresAt = now() + (value?.ttl ?: 0) - MIN_TTL
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable {
|
override fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable {
|
||||||
if (cachedTurnServerResponse.data != null) {
|
return taskExecutor.executorScope.launchToCallback(Dispatchers.Default, callback) {
|
||||||
cachedTurnServerResponse.data?.let { callback.onSuccess(it) }
|
turnServerDataSource.getTurnServer()
|
||||||
return NoOpCancellable
|
|
||||||
}
|
}
|
||||||
return turnServerTask
|
|
||||||
.configureWith(GetTurnServerTask.Params) {
|
|
||||||
this.callback = object : MatrixCallback<TurnServerResponse> {
|
|
||||||
override fun onSuccess(data: TurnServerResponse) {
|
|
||||||
cachedTurnServerResponse.data = data
|
|
||||||
callback.onSuccess(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
callback.onFailure(failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall {
|
override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall {
|
||||||
val call = MxCallImpl(
|
return mxCallFactory.createOutgoingCall(roomId, otherUserId, isVideoCall).also {
|
||||||
callId = UUID.randomUUID().toString(),
|
activeCallHandler.addCall(it)
|
||||||
isOutgoing = true,
|
|
||||||
roomId = roomId,
|
|
||||||
userId = userId,
|
|
||||||
otherUserId = otherUserId,
|
|
||||||
isVideoCall = isVideoCall,
|
|
||||||
localEchoEventFactory = localEchoEventFactory,
|
|
||||||
eventSenderProcessor = eventSenderProcessor
|
|
||||||
)
|
|
||||||
activeCallHandler.addCall(call).also {
|
|
||||||
return call
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addCallListener(listener: CallsListener) {
|
override fun addCallListener(listener: CallListener) {
|
||||||
callListeners.add(listener)
|
callSignalingHandler.addCallListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeCallListener(listener: CallsListener) {
|
override fun removeCallListener(listener: CallListener) {
|
||||||
callListeners.remove(listener)
|
callSignalingHandler.removeCallListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCallWithId(callId: String): MxCall? {
|
override fun getCallWithId(callId: String): MxCall? {
|
||||||
|
@ -127,129 +67,6 @@ internal class DefaultCallSignalingService @Inject constructor(
|
||||||
return activeCallHandler.getActiveCallsLiveData().value?.isNotEmpty() == true
|
return activeCallHandler.getActiveCallsLiveData().value?.isNotEmpty() == true
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun onCallEvent(event: Event) {
|
|
||||||
when (event.getClearType()) {
|
|
||||||
EventType.CALL_ANSWER -> {
|
|
||||||
event.getClearContent().toModel<CallAnswerContent>()?.let {
|
|
||||||
if (event.senderId == userId) {
|
|
||||||
// ok it's an answer from me.. is it remote echo or other session
|
|
||||||
val knownCall = getCallWithId(it.callId)
|
|
||||||
if (knownCall == null) {
|
|
||||||
Timber.d("## VOIP onCallEvent ${event.getClearType()} id ${it.callId} send by me")
|
|
||||||
} else if (!knownCall.isOutgoing) {
|
|
||||||
// incoming call
|
|
||||||
// if it was anwsered by this session, the call state would be in Answering(or connected) state
|
|
||||||
if (knownCall.state == CallState.LocalRinging) {
|
|
||||||
// discard current call, it's answered by another of my session
|
|
||||||
onCallManageByOtherSession(it.callId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
onCallAnswer(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EventType.CALL_INVITE -> {
|
|
||||||
if (event.senderId == userId) {
|
|
||||||
// Always ignore local echos of invite
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
event.getClearContent().toModel<CallInviteContent>()?.let { content ->
|
|
||||||
val incomingCall = MxCallImpl(
|
|
||||||
callId = content.callId ?: return@let,
|
|
||||||
isOutgoing = false,
|
|
||||||
roomId = event.roomId ?: return@let,
|
|
||||||
userId = userId,
|
|
||||||
otherUserId = event.senderId ?: return@let,
|
|
||||||
isVideoCall = content.isVideo(),
|
|
||||||
localEchoEventFactory = localEchoEventFactory,
|
|
||||||
eventSenderProcessor = eventSenderProcessor
|
|
||||||
)
|
|
||||||
activeCallHandler.addCall(incomingCall)
|
|
||||||
onCallInvite(incomingCall, content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EventType.CALL_HANGUP -> {
|
|
||||||
event.getClearContent().toModel<CallHangupContent>()?.let { content ->
|
|
||||||
|
|
||||||
if (event.senderId == userId) {
|
|
||||||
// ok it's an answer from me.. is it remote echo or other session
|
|
||||||
val knownCall = getCallWithId(content.callId)
|
|
||||||
if (knownCall == null) {
|
|
||||||
Timber.d("## VOIP onCallEvent ${event.getClearType()} id ${content.callId} send by me")
|
|
||||||
} else if (!knownCall.isOutgoing) {
|
|
||||||
// incoming call
|
|
||||||
if (knownCall.state == CallState.LocalRinging) {
|
|
||||||
// discard current call, it's answered by another of my session
|
|
||||||
onCallManageByOtherSession(content.callId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
activeCallHandler.removeCall(content.callId)
|
|
||||||
onCallHangup(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EventType.CALL_CANDIDATES -> {
|
|
||||||
if (event.senderId == userId) {
|
|
||||||
// Always ignore local echos of invite
|
|
||||||
return
|
|
||||||
}
|
|
||||||
event.getClearContent().toModel<CallCandidatesContent>()?.let { content ->
|
|
||||||
activeCallHandler.getCallWithId(content.callId)?.let {
|
|
||||||
onCallIceCandidate(it, content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCallHangup(hangup: CallHangupContent) {
|
|
||||||
callListeners.toList().forEach {
|
|
||||||
tryOrNull {
|
|
||||||
it.onCallHangupReceived(hangup)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCallAnswer(answer: CallAnswerContent) {
|
|
||||||
callListeners.toList().forEach {
|
|
||||||
tryOrNull {
|
|
||||||
it.onCallAnswerReceived(answer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCallManageByOtherSession(callId: String) {
|
|
||||||
callListeners.toList().forEach {
|
|
||||||
tryOrNull {
|
|
||||||
it.onCallManagedByOtherSession(callId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCallInvite(incomingCall: MxCall, invite: CallInviteContent) {
|
|
||||||
// Ignore the invitation from current user
|
|
||||||
if (incomingCall.otherUserId == userId) return
|
|
||||||
|
|
||||||
callListeners.toList().forEach {
|
|
||||||
tryOrNull {
|
|
||||||
it.onCallInviteReceived(incomingCall, invite)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCallIceCandidate(incomingCall: MxCall, candidates: CallCandidatesContent) {
|
|
||||||
callListeners.toList().forEach {
|
|
||||||
tryOrNull {
|
|
||||||
it.onCallIceCandidateReceived(incomingCall, candidates)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CALL_TIMEOUT_MS = 120_000
|
const val CALL_TIMEOUT_MS = 120_000
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.session.call
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||||
|
import org.matrix.android.sdk.api.session.call.MxCall
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
import org.matrix.android.sdk.internal.di.DeviceId
|
||||||
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
|
import org.matrix.android.sdk.internal.session.call.model.MxCallImpl
|
||||||
|
import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask
|
||||||
|
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||||
|
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class MxCallFactory @Inject constructor(
|
||||||
|
@DeviceId private val deviceId: String?,
|
||||||
|
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||||
|
private val eventSenderProcessor: EventSenderProcessor,
|
||||||
|
private val matrixConfiguration: MatrixConfiguration,
|
||||||
|
private val getProfileInfoTask: GetProfileInfoTask,
|
||||||
|
@UserId private val userId: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun createIncomingCall(roomId: String, opponentUserId: String, content: CallInviteContent): MxCall? {
|
||||||
|
content.callId ?: return null
|
||||||
|
return MxCallImpl(
|
||||||
|
callId = content.callId,
|
||||||
|
isOutgoing = false,
|
||||||
|
roomId = roomId,
|
||||||
|
userId = userId,
|
||||||
|
ourPartyId = deviceId ?: "",
|
||||||
|
opponentUserId = opponentUserId,
|
||||||
|
isVideoCall = content.isVideo(),
|
||||||
|
localEchoEventFactory = localEchoEventFactory,
|
||||||
|
eventSenderProcessor = eventSenderProcessor,
|
||||||
|
matrixConfiguration = matrixConfiguration,
|
||||||
|
getProfileInfoTask = getProfileInfoTask
|
||||||
|
).apply {
|
||||||
|
opponentPartyId = Optional.from(content.partyId)
|
||||||
|
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
|
||||||
|
capabilities = content.capabilities ?: CallCapabilities()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createOutgoingCall(roomId: String, opponentUserId: String, isVideoCall: Boolean): MxCall {
|
||||||
|
return MxCallImpl(
|
||||||
|
callId = UUID.randomUUID().toString(),
|
||||||
|
isOutgoing = true,
|
||||||
|
roomId = roomId,
|
||||||
|
userId = userId,
|
||||||
|
ourPartyId = deviceId ?: "",
|
||||||
|
opponentUserId = opponentUserId,
|
||||||
|
isVideoCall = isVideoCall,
|
||||||
|
localEchoEventFactory = localEchoEventFactory,
|
||||||
|
eventSenderProcessor = eventSenderProcessor,
|
||||||
|
matrixConfiguration = matrixConfiguration,
|
||||||
|
getProfileInfoTask = getProfileInfoTask
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.session.call
|
||||||
|
|
||||||
|
import android.os.SystemClock
|
||||||
|
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class TurnServerDataSource @Inject constructor(private val turnServerTask: GetTurnServerTask) {
|
||||||
|
|
||||||
|
private val cachedTurnServerResponse = object {
|
||||||
|
// Keep one minute safe to avoid considering the data is valid and then actually it is not when effectively using it.
|
||||||
|
private val MIN_TTL = 60
|
||||||
|
|
||||||
|
private val now = { SystemClock.elapsedRealtime() / 1000 }
|
||||||
|
|
||||||
|
private var expiresAt: Long = 0
|
||||||
|
|
||||||
|
var data: TurnServerResponse? = null
|
||||||
|
get() = if (expiresAt > now()) field else null
|
||||||
|
set(value) {
|
||||||
|
expiresAt = now() + (value?.ttl ?: 0) - MIN_TTL
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getTurnServer(): TurnServerResponse {
|
||||||
|
return cachedTurnServerResponse.data ?: turnServerTask.execute(GetTurnServerTask.Params).also {
|
||||||
|
cachedTurnServerResponse.data = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.call.model
|
package org.matrix.android.sdk.internal.session.call.model
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
import org.matrix.android.sdk.api.session.call.CallState
|
||||||
import org.matrix.android.sdk.api.session.call.MxCall
|
import org.matrix.android.sdk.api.session.call.MxCall
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
|
@ -24,28 +25,44 @@ import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.LocalEcho
|
import org.matrix.android.sdk.api.session.events.model.LocalEcho
|
||||||
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||||
|
import org.matrix.android.sdk.api.session.profile.ProfileService
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallCandidate
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.SdpType
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService
|
import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService
|
||||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask
|
||||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||||
import org.webrtc.IceCandidate
|
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||||
import org.webrtc.SessionDescription
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
internal class MxCallImpl(
|
internal class MxCallImpl(
|
||||||
override val callId: String,
|
override val callId: String,
|
||||||
override val isOutgoing: Boolean,
|
override val isOutgoing: Boolean,
|
||||||
override val roomId: String,
|
override val roomId: String,
|
||||||
private val userId: String,
|
private val userId: String,
|
||||||
override val otherUserId: String,
|
override val opponentUserId: String,
|
||||||
override val isVideoCall: Boolean,
|
override val isVideoCall: Boolean,
|
||||||
|
override val ourPartyId: String,
|
||||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||||
private val eventSenderProcessor: EventSenderProcessor
|
private val eventSenderProcessor: EventSenderProcessor,
|
||||||
|
private val matrixConfiguration: MatrixConfiguration,
|
||||||
|
private val getProfileInfoTask: GetProfileInfoTask
|
||||||
) : MxCall {
|
) : MxCall {
|
||||||
|
|
||||||
|
override var opponentPartyId: Optional<String>? = null
|
||||||
|
override var opponentVersion: Int = MxCall.VOIP_PROTO_VERSION
|
||||||
|
override var capabilities: CallCapabilities? = null
|
||||||
|
|
||||||
override var state: CallState = CallState.Idle
|
override var state: CallState = CallState.Idle
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
|
@ -81,60 +98,135 @@ internal class MxCallImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun offerSdp(sdp: SessionDescription) {
|
override fun offerSdp(sdpString: String) {
|
||||||
if (!isOutgoing) return
|
if (!isOutgoing) return
|
||||||
Timber.v("## VOIP offerSdp $callId")
|
Timber.v("## VOIP offerSdp $callId")
|
||||||
state = CallState.Dialing
|
state = CallState.Dialing
|
||||||
CallInviteContent(
|
CallInviteContent(
|
||||||
callId = callId,
|
callId = callId,
|
||||||
|
partyId = ourPartyId,
|
||||||
lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS,
|
lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS,
|
||||||
offer = CallInviteContent.Offer(sdp = sdp.description)
|
offer = CallInviteContent.Offer(sdp = sdpString),
|
||||||
|
version = MxCall.VOIP_PROTO_VERSION.toString(),
|
||||||
|
capabilities = buildCapabilities()
|
||||||
)
|
)
|
||||||
.let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) }
|
.let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) }
|
||||||
.also { eventSenderProcessor.postEvent(it) }
|
.also { eventSenderProcessor.postEvent(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendLocalIceCandidates(candidates: List<IceCandidate>) {
|
override fun sendLocalCallCandidates(candidates: List<CallCandidate>) {
|
||||||
|
Timber.v("Send local call canditates $callId: $candidates")
|
||||||
CallCandidatesContent(
|
CallCandidatesContent(
|
||||||
callId = callId,
|
callId = callId,
|
||||||
candidates = candidates.map {
|
partyId = ourPartyId,
|
||||||
CallCandidatesContent.Candidate(
|
candidates = candidates,
|
||||||
sdpMid = it.sdpMid,
|
version = MxCall.VOIP_PROTO_VERSION.toString()
|
||||||
sdpMLineIndex = it.sdpMLineIndex,
|
|
||||||
candidate = it.sdp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
.let { createEventAndLocalEcho(type = EventType.CALL_CANDIDATES, roomId = roomId, content = it.toContent()) }
|
.let { createEventAndLocalEcho(type = EventType.CALL_CANDIDATES, roomId = roomId, content = it.toContent()) }
|
||||||
.also { eventSenderProcessor.postEvent(it) }
|
.also { eventSenderProcessor.postEvent(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>) {
|
override fun sendLocalIceCandidateRemovals(candidates: List<CallCandidate>) {
|
||||||
// For now we don't support this flow
|
// For now we don't support this flow
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hangUp() {
|
override fun reject() {
|
||||||
|
if (opponentVersion < 1) {
|
||||||
|
Timber.v("Opponent version is less than 1 ($opponentVersion): sending hangup instead of reject")
|
||||||
|
hangUp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Timber.v("## VOIP reject $callId")
|
||||||
|
CallRejectContent(
|
||||||
|
callId = callId,
|
||||||
|
partyId = ourPartyId,
|
||||||
|
version = MxCall.VOIP_PROTO_VERSION.toString()
|
||||||
|
)
|
||||||
|
.let { createEventAndLocalEcho(type = EventType.CALL_REJECT, roomId = roomId, content = it.toContent()) }
|
||||||
|
.also { eventSenderProcessor.postEvent(it) }
|
||||||
|
state = CallState.Terminated
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hangUp(reason: CallHangupContent.Reason?) {
|
||||||
Timber.v("## VOIP hangup $callId")
|
Timber.v("## VOIP hangup $callId")
|
||||||
CallHangupContent(
|
CallHangupContent(
|
||||||
callId = callId
|
callId = callId,
|
||||||
|
partyId = ourPartyId,
|
||||||
|
reason = reason ?: CallHangupContent.Reason.USER_HANGUP,
|
||||||
|
version = MxCall.VOIP_PROTO_VERSION.toString()
|
||||||
)
|
)
|
||||||
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
|
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
|
||||||
.also { eventSenderProcessor.postEvent(it) }
|
.also { eventSenderProcessor.postEvent(it) }
|
||||||
state = CallState.Terminated
|
state = CallState.Terminated
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun accept(sdp: SessionDescription) {
|
override fun accept(sdpString: String) {
|
||||||
Timber.v("## VOIP accept $callId")
|
Timber.v("## VOIP accept $callId")
|
||||||
if (isOutgoing) return
|
if (isOutgoing) return
|
||||||
state = CallState.Answering
|
state = CallState.Answering
|
||||||
CallAnswerContent(
|
CallAnswerContent(
|
||||||
callId = callId,
|
callId = callId,
|
||||||
answer = CallAnswerContent.Answer(sdp = sdp.description)
|
partyId = ourPartyId,
|
||||||
|
answer = CallAnswerContent.Answer(sdp = sdpString),
|
||||||
|
version = MxCall.VOIP_PROTO_VERSION.toString(),
|
||||||
|
capabilities = buildCapabilities()
|
||||||
)
|
)
|
||||||
.let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) }
|
.let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) }
|
||||||
.also { eventSenderProcessor.postEvent(it) }
|
.also { eventSenderProcessor.postEvent(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun negotiate(sdpString: String, type: SdpType) {
|
||||||
|
Timber.v("## VOIP negotiate $callId")
|
||||||
|
CallNegotiateContent(
|
||||||
|
callId = callId,
|
||||||
|
partyId = ourPartyId,
|
||||||
|
lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS,
|
||||||
|
description = CallNegotiateContent.Description(sdp = sdpString, type = type),
|
||||||
|
version = MxCall.VOIP_PROTO_VERSION.toString()
|
||||||
|
)
|
||||||
|
.let { createEventAndLocalEcho(type = EventType.CALL_NEGOTIATE, roomId = roomId, content = it.toContent()) }
|
||||||
|
.also { eventSenderProcessor.postEvent(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun selectAnswer() {
|
||||||
|
Timber.v("## VOIP select answer $callId")
|
||||||
|
if (isOutgoing) return
|
||||||
|
state = CallState.Answering
|
||||||
|
CallSelectAnswerContent(
|
||||||
|
callId = callId,
|
||||||
|
partyId = ourPartyId,
|
||||||
|
selectedPartyId = opponentPartyId?.getOrNull(),
|
||||||
|
version = MxCall.VOIP_PROTO_VERSION.toString()
|
||||||
|
)
|
||||||
|
.let { createEventAndLocalEcho(type = EventType.CALL_SELECT_ANSWER, roomId = roomId, content = it.toContent()) }
|
||||||
|
.also { eventSenderProcessor.postEvent(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun transfer(targetUserId: String, targetRoomId: String?) {
|
||||||
|
val profileInfoParams = GetProfileInfoTask.Params(targetUserId)
|
||||||
|
val profileInfo = try {
|
||||||
|
getProfileInfoTask.execute(profileInfoParams)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.v("Fail fetching profile info of $targetUserId while transferring call")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
CallReplacesContent(
|
||||||
|
callId = callId,
|
||||||
|
partyId = ourPartyId,
|
||||||
|
replacementId = UUID.randomUUID().toString(),
|
||||||
|
version = MxCall.VOIP_PROTO_VERSION.toString(),
|
||||||
|
targetUser = CallReplacesContent.TargetUser(
|
||||||
|
id = targetUserId,
|
||||||
|
displayName = profileInfo?.get(ProfileService.DISPLAY_NAME_KEY) as? String,
|
||||||
|
avatarUrl = profileInfo?.get(ProfileService.AVATAR_URL_KEY) as? String
|
||||||
|
),
|
||||||
|
targerRoomId = targetRoomId,
|
||||||
|
createCall = UUID.randomUUID().toString()
|
||||||
|
)
|
||||||
|
.let { createEventAndLocalEcho(type = EventType.CALL_REPLACES, roomId = roomId, content = it.toContent()) }
|
||||||
|
.also { eventSenderProcessor.postEvent(it) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event {
|
private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event {
|
||||||
return Event(
|
return Event(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
|
@ -147,4 +239,12 @@ internal class MxCallImpl(
|
||||||
)
|
)
|
||||||
.also { localEchoEventFactory.createLocalEcho(it) }
|
.also { localEchoEventFactory.createLocalEcho(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildCapabilities(): CallCapabilities? {
|
||||||
|
return if (matrixConfiguration.supportsCallTransfer) {
|
||||||
|
CallCapabilities(true)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,9 @@ import org.matrix.android.sdk.api.session.room.RoomDirectoryService
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
||||||
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
|
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
|
||||||
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse
|
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse
|
||||||
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
|
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask
|
import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask
|
import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.GetThirdPartyProtocolsTask
|
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask
|
import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||||
import org.matrix.android.sdk.internal.task.configureWith
|
import org.matrix.android.sdk.internal.task.configureWith
|
||||||
|
@ -33,7 +31,6 @@ import javax.inject.Inject
|
||||||
|
|
||||||
internal class DefaultRoomDirectoryService @Inject constructor(
|
internal class DefaultRoomDirectoryService @Inject constructor(
|
||||||
private val getPublicRoomTask: GetPublicRoomTask,
|
private val getPublicRoomTask: GetPublicRoomTask,
|
||||||
private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask,
|
|
||||||
private val getRoomDirectoryVisibilityTask: GetRoomDirectoryVisibilityTask,
|
private val getRoomDirectoryVisibilityTask: GetRoomDirectoryVisibilityTask,
|
||||||
private val setRoomDirectoryVisibilityTask: SetRoomDirectoryVisibilityTask,
|
private val setRoomDirectoryVisibilityTask: SetRoomDirectoryVisibilityTask,
|
||||||
private val taskExecutor: TaskExecutor) : RoomDirectoryService {
|
private val taskExecutor: TaskExecutor) : RoomDirectoryService {
|
||||||
|
@ -48,14 +45,6 @@ internal class DefaultRoomDirectoryService @Inject constructor(
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable {
|
|
||||||
return getThirdPartyProtocolsTask
|
|
||||||
.configureWith {
|
|
||||||
this.callback = callback
|
|
||||||
}
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getRoomDirectoryVisibility(roomId: String): RoomDirectoryVisibility {
|
override suspend fun getRoomDirectoryVisibility(roomId: String): RoomDirectoryVisibility {
|
||||||
return getRoomDirectoryVisibilityTask.execute(GetRoomDirectoryVisibilityTask.Params(roomId))
|
return getRoomDirectoryVisibilityTask.execute(GetRoomDirectoryVisibilityTask.Params(roomId))
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
|
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
|
||||||
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse
|
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse
|
||||||
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
|
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
import org.matrix.android.sdk.internal.network.NetworkConstants
|
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||||
import org.matrix.android.sdk.internal.session.room.alias.GetAliasesResponse
|
import org.matrix.android.sdk.internal.session.room.alias.GetAliasesResponse
|
||||||
|
@ -50,14 +49,6 @@ import retrofit2.http.Query
|
||||||
|
|
||||||
internal interface RoomAPI {
|
internal interface RoomAPI {
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the third party server protocols.
|
|
||||||
*
|
|
||||||
* Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-thirdparty-protocols
|
|
||||||
*/
|
|
||||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols")
|
|
||||||
fun thirdPartyProtocols(): Call<Map<String, ThirdPartyProtocol>>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists the public rooms on the server, with optional filter.
|
* Lists the public rooms on the server, with optional filter.
|
||||||
* This API returns paginated responses. The rooms are ordered by the number of joined members, with the largest rooms first.
|
* This API returns paginated responses. The rooms are ordered by the number of joined members, with the largest rooms first.
|
||||||
|
|
|
@ -39,11 +39,9 @@ import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
|
||||||
import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask
|
import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.DefaultGetPublicRoomTask
|
import org.matrix.android.sdk.internal.session.room.directory.DefaultGetPublicRoomTask
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.DefaultGetRoomDirectoryVisibilityTask
|
import org.matrix.android.sdk.internal.session.room.directory.DefaultGetRoomDirectoryVisibilityTask
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.DefaultGetThirdPartyProtocolsTask
|
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDirectoryVisibilityTask
|
import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDirectoryVisibilityTask
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask
|
import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask
|
import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.GetThirdPartyProtocolsTask
|
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask
|
import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask
|
import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
|
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
|
||||||
|
@ -153,9 +151,6 @@ internal abstract class RoomModule {
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindSetRoomDirectoryVisibilityTask(task: DefaultSetRoomDirectoryVisibilityTask): SetRoomDirectoryVisibilityTask
|
abstract fun bindSetRoomDirectoryVisibilityTask(task: DefaultSetRoomDirectoryVisibilityTask): SetRoomDirectoryVisibilityTask
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindGetThirdPartyProtocolsTask(task: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask
|
abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask
|
||||||
|
|
||||||
|
|
|
@ -49,8 +49,10 @@ internal class QueueMemento @Inject constructor(context: Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unTrack(task: QueuedTask) {
|
fun unTrack(task: QueuedTask) {
|
||||||
managedTaskInfos.remove(task)
|
synchronized(managedTaskInfos) {
|
||||||
persist()
|
managedTaskInfos.remove(task)
|
||||||
|
persist()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun persist() {
|
private fun persist() {
|
||||||
|
@ -64,19 +66,17 @@ internal class QueueMemento @Inject constructor(context: Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toTaskInfo(task: QueuedTask, order: Int): TaskInfo? {
|
private fun toTaskInfo(task: QueuedTask, order: Int): TaskInfo? {
|
||||||
synchronized(managedTaskInfos) {
|
return when (task) {
|
||||||
return when (task) {
|
is SendEventQueuedTask -> SendEventTaskInfo(
|
||||||
is SendEventQueuedTask -> SendEventTaskInfo(
|
localEchoId = task.event.eventId ?: "",
|
||||||
localEchoId = task.event.eventId ?: "",
|
encrypt = task.encrypt,
|
||||||
encrypt = task.encrypt,
|
order = order
|
||||||
order = order
|
)
|
||||||
)
|
is RedactQueuedTask -> RedactEventTaskInfo(
|
||||||
is RedactQueuedTask -> RedactEventTaskInfo(
|
redactionLocalEcho = task.redactionLocalEchoId,
|
||||||
redactionLocalEcho = task.redactionLocalEchoId,
|
order = order
|
||||||
order = order
|
)
|
||||||
)
|
else -> null
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ internal class QueueMemento @Inject constructor(context: Context,
|
||||||
?.forEach { info ->
|
?.forEach { info ->
|
||||||
try {
|
try {
|
||||||
when (info) {
|
when (info) {
|
||||||
is SendEventTaskInfo -> {
|
is SendEventTaskInfo -> {
|
||||||
localEchoRepository.getUpToDateEcho(info.localEchoId)?.let {
|
localEchoRepository.getUpToDateEcho(info.localEchoId)?.let {
|
||||||
if (it.sendState.isSending() && it.eventId != null && it.roomId != null) {
|
if (it.sendState.isSending() && it.eventId != null && it.roomId != null) {
|
||||||
localEchoRepository.updateSendState(it.eventId, it.roomId, SendState.UNSENT)
|
localEchoRepository.updateSendState(it.eventId, it.roomId, SendState.UNSENT)
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 The Matrix.org Foundation C.I.C
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.session.thirdparty
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||||
|
import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService
|
||||||
|
import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class DefaultThirdPartyService @Inject constructor(private val getThirdPartyProtocolTask: GetThirdPartyProtocolsTask,
|
||||||
|
private val getThirdPartyUserTask: GetThirdPartyUserTask)
|
||||||
|
: ThirdPartyService {
|
||||||
|
|
||||||
|
override suspend fun getThirdPartyProtocols(): Map<String, ThirdPartyProtocol> {
|
||||||
|
return getThirdPartyProtocolTask.execute(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getThirdPartyUser(protocol: String, fields: Map<String, String>): List<ThirdPartyUser> {
|
||||||
|
val taskParams = GetThirdPartyUserTask.Params(
|
||||||
|
protocol = protocol,
|
||||||
|
fields = fields
|
||||||
|
)
|
||||||
|
return getThirdPartyUserTask.execute(taskParams)
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,25 +14,24 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.room.directory
|
package org.matrix.android.sdk.internal.session.thirdparty
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
|
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface GetThirdPartyProtocolsTask : Task<Unit, Map<String, ThirdPartyProtocol>>
|
internal interface GetThirdPartyProtocolsTask : Task<Unit, Map<String, ThirdPartyProtocol>>
|
||||||
|
|
||||||
internal class DefaultGetThirdPartyProtocolsTask @Inject constructor(
|
internal class DefaultGetThirdPartyProtocolsTask @Inject constructor(
|
||||||
private val roomAPI: RoomAPI,
|
private val thirdPartyAPI: ThirdPartyAPI,
|
||||||
private val globalErrorReceiver: GlobalErrorReceiver
|
private val globalErrorReceiver: GlobalErrorReceiver
|
||||||
) : GetThirdPartyProtocolsTask {
|
) : GetThirdPartyProtocolsTask {
|
||||||
|
|
||||||
override suspend fun execute(params: Unit): Map<String, ThirdPartyProtocol> {
|
override suspend fun execute(params: Unit): Map<String, ThirdPartyProtocol> {
|
||||||
return executeRequest(globalErrorReceiver) {
|
return executeRequest(globalErrorReceiver) {
|
||||||
apiCall = roomAPI.thirdPartyProtocols()
|
apiCall = thirdPartyAPI.thirdPartyProtocols()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.session.thirdparty
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser
|
||||||
|
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||||
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface GetThirdPartyUserTask : Task<GetThirdPartyUserTask.Params, List<ThirdPartyUser>> {
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val protocol: String,
|
||||||
|
val fields: Map<String, String> = emptyMap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultGetThirdPartyUserTask @Inject constructor(
|
||||||
|
private val thirdPartyAPI: ThirdPartyAPI,
|
||||||
|
private val globalErrorReceiver: GlobalErrorReceiver
|
||||||
|
) : GetThirdPartyUserTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: GetThirdPartyUserTask.Params): List<ThirdPartyUser> {
|
||||||
|
return executeRequest(globalErrorReceiver) {
|
||||||
|
apiCall = thirdPartyAPI.getThirdPartyUser(params.protocol, params.fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.session.thirdparty
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||||
|
import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser
|
||||||
|
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.QueryMap
|
||||||
|
|
||||||
|
internal interface ThirdPartyAPI {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the third party server protocols.
|
||||||
|
*
|
||||||
|
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1.html#get-matrix-client-r0-thirdparty-protocols
|
||||||
|
*/
|
||||||
|
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols")
|
||||||
|
fun thirdPartyProtocols(): Call<Map<String, ThirdPartyProtocol>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a Matrix User ID linked to a user on the third party service, given a set of user parameters.
|
||||||
|
*
|
||||||
|
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-thirdparty-user-protocol
|
||||||
|
*/
|
||||||
|
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols/user/{protocol}")
|
||||||
|
fun getThirdPartyUser(@Path("protocol") protocol: String, @QueryMap params: Map<String, String>?): Call<List<ThirdPartyUser>>
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.session.thirdparty
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService
|
||||||
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
|
@Module
|
||||||
|
internal abstract class ThirdPartyModule {
|
||||||
|
|
||||||
|
@Module
|
||||||
|
companion object {
|
||||||
|
@Provides
|
||||||
|
@JvmStatic
|
||||||
|
@SessionScope
|
||||||
|
fun providesThirdPartyAPI(retrofit: Retrofit): ThirdPartyAPI {
|
||||||
|
return retrofit.create(ThirdPartyAPI::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindThirdPartyService(service: DefaultThirdPartyService): ThirdPartyService
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindGetThirdPartyProtocolsTask(task: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindGetThirdPartyUserTask(task: DefaultGetThirdPartyUserTask): GetThirdPartyUserTask
|
||||||
|
}
|
|
@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1
|
||||||
# android\.text\.TextUtils
|
# android\.text\.TextUtils
|
||||||
|
|
||||||
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
||||||
enum class===85
|
enum class===88
|
||||||
|
|
||||||
### Do not import temporary legacy classes
|
### Do not import temporary legacy classes
|
||||||
import org.matrix.android.sdk.internal.legacy.riot===3
|
import org.matrix.android.sdk.internal.legacy.riot===3
|
||||||
|
|
|
@ -114,6 +114,9 @@ android {
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
|
renderscriptTargetApi 24
|
||||||
|
renderscriptSupportModeEnabled true
|
||||||
|
|
||||||
// `develop` branch will have version code from timestamp, to ensure each build from CI has a incremented versionCode.
|
// `develop` branch will have version code from timestamp, to ensure each build from CI has a incremented versionCode.
|
||||||
// Other branches (master, features, etc.) will have version code based on application version.
|
// Other branches (master, features, etc.) will have version code based on application version.
|
||||||
versionCode project.getVersionCode()
|
versionCode project.getVersionCode()
|
||||||
|
@ -232,7 +235,7 @@ android {
|
||||||
productFlavors {
|
productFlavors {
|
||||||
gplay {
|
gplay {
|
||||||
dimension "store"
|
dimension "store"
|
||||||
|
isDefault = true
|
||||||
versionName "${versionMajor}.${versionMinor}.${versionPatch}${getGplayVersionSuffix()}"
|
versionName "${versionMajor}.${versionMinor}.${versionPatch}${getGplayVersionSuffix()}"
|
||||||
|
|
||||||
resValue "bool", "isGplay", "true"
|
resValue "bool", "isGplay", "true"
|
||||||
|
@ -319,6 +322,7 @@ dependencies {
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation "androidx.sharetarget:sharetarget:1.0.0"
|
implementation "androidx.sharetarget:sharetarget:1.0.0"
|
||||||
implementation 'androidx.core:core-ktx:1.3.2'
|
implementation 'androidx.core:core-ktx:1.3.2'
|
||||||
|
implementation "androidx.media:media:1.2.1"
|
||||||
|
|
||||||
implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
|
implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
|
||||||
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0"
|
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0"
|
||||||
|
@ -373,6 +377,7 @@ dependencies {
|
||||||
implementation 'me.saket:better-link-movement-method:2.2.0'
|
implementation 'me.saket:better-link-movement-method:2.2.0'
|
||||||
implementation 'com.google.android:flexbox:1.1.1'
|
implementation 'com.google.android:flexbox:1.1.1'
|
||||||
implementation "androidx.autofill:autofill:$autofill_version"
|
implementation "androidx.autofill:autofill:$autofill_version"
|
||||||
|
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||||
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
|
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
|
||||||
|
|
||||||
// Custom Tab
|
// Custom Tab
|
||||||
|
@ -441,6 +446,8 @@ dependencies {
|
||||||
implementation 'com.vanniktech:emoji-material:0.7.0'
|
implementation 'com.vanniktech:emoji-material:0.7.0'
|
||||||
implementation 'com.vanniktech:emoji-google:0.7.0'
|
implementation 'com.vanniktech:emoji-google:0.7.0'
|
||||||
|
|
||||||
|
implementation 'im.dlg:android-dialer:1.2.5'
|
||||||
|
|
||||||
// TESTS
|
// TESTS
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13'
|
||||||
testImplementation "org.amshove.kluent:kluent-android:$kluent_version"
|
testImplementation "org.amshove.kluent:kluent-android:$kluent_version"
|
||||||
|
|
|
@ -230,7 +230,8 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".features.attachments.preview.AttachmentsPreviewActivity"
|
android:name=".features.attachments.preview.AttachmentsPreviewActivity"
|
||||||
android:theme="@style/AppTheme.AttachmentsPreview" />
|
android:theme="@style/AppTheme.AttachmentsPreview" />
|
||||||
<activity android:name=".features.call.VectorCallActivity" />
|
<activity android:name=".features.call.VectorCallActivity"
|
||||||
|
android:excludeFromRecents="true"/>
|
||||||
<activity
|
<activity
|
||||||
android:name=".features.call.conference.VectorJitsiActivity"
|
android:name=".features.call.conference.VectorJitsiActivity"
|
||||||
android:configChanges="orientation|screenSize" />
|
android:configChanges="orientation|screenSize" />
|
||||||
|
@ -240,6 +241,7 @@
|
||||||
<activity android:name=".features.pin.PinActivity" />
|
<activity android:name=".features.pin.PinActivity" />
|
||||||
<activity android:name=".features.home.room.detail.search.SearchActivity" />
|
<activity android:name=".features.home.room.detail.search.SearchActivity" />
|
||||||
<activity android:name=".features.usercode.UserCodeActivity" />
|
<activity android:name=".features.usercode.UserCodeActivity" />
|
||||||
|
<activity android:name=".features.call.transfer.CallTransferActivity" />
|
||||||
|
|
||||||
<!-- Single instance is very important for the custom scheme callback-->
|
<!-- Single instance is very important for the custom scheme callback-->
|
||||||
<activity android:name=".features.auth.ReAuthActivity"
|
<activity android:name=".features.auth.ReAuthActivity"
|
||||||
|
|
|
@ -385,6 +385,11 @@ SOFTWARE.
|
||||||
<br/>
|
<br/>
|
||||||
Copyright 2016 JetRadar
|
Copyright 2016 JetRadar
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>dialogs / android-dialer</b>
|
||||||
|
<br/>
|
||||||
|
Copyright (c) 2017-present, dialog LLC <info@dlg.im>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<pre>
|
<pre>
|
||||||
Apache License
|
Apache License
|
||||||
|
|
|
@ -44,7 +44,7 @@ import im.vector.app.core.di.HasVectorInjector
|
||||||
import im.vector.app.core.di.VectorComponent
|
import im.vector.app.core.di.VectorComponent
|
||||||
import im.vector.app.core.extensions.configureAndStart
|
import im.vector.app.core.extensions.configureAndStart
|
||||||
import im.vector.app.core.rx.RxConfig
|
import im.vector.app.core.rx.RxConfig
|
||||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
import im.vector.app.features.configuration.VectorConfiguration
|
import im.vector.app.features.configuration.VectorConfiguration
|
||||||
import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog
|
import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog
|
||||||
import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks
|
import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks
|
||||||
|
@ -92,7 +92,7 @@ class VectorApplication :
|
||||||
@Inject lateinit var rxConfig: RxConfig
|
@Inject lateinit var rxConfig: RxConfig
|
||||||
@Inject lateinit var popupAlertManager: PopupAlertManager
|
@Inject lateinit var popupAlertManager: PopupAlertManager
|
||||||
@Inject lateinit var pinLocker: PinLocker
|
@Inject lateinit var pinLocker: PinLocker
|
||||||
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
@Inject lateinit var callManager: WebRtcCallManager
|
||||||
|
|
||||||
lateinit var vectorComponent: VectorComponent
|
lateinit var vectorComponent: VectorComponent
|
||||||
|
|
||||||
|
@ -177,7 +177,7 @@ class VectorApplication :
|
||||||
})
|
})
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker)
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(webRtcPeerConnectionManager)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(callManager)
|
||||||
// This should be done as early as possible
|
// This should be done as early as possible
|
||||||
// initKnownEmojiHashSet(appContext)
|
// initKnownEmojiHashSet(appContext)
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ package im.vector.app.core.di
|
||||||
|
|
||||||
import arrow.core.Option
|
import arrow.core.Option
|
||||||
import im.vector.app.ActiveSessionDataSource
|
import im.vector.app.ActiveSessionDataSource
|
||||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
|
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
|
||||||
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
|
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
|
||||||
import im.vector.app.features.notifications.PushRuleTriggerListener
|
import im.vector.app.features.notifications.PushRuleTriggerListener
|
||||||
|
@ -35,7 +35,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService:
|
||||||
private val sessionObservableStore: ActiveSessionDataSource,
|
private val sessionObservableStore: ActiveSessionDataSource,
|
||||||
private val keyRequestHandler: KeyRequestHandler,
|
private val keyRequestHandler: KeyRequestHandler,
|
||||||
private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler,
|
private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler,
|
||||||
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
|
private val callManager: WebRtcCallManager,
|
||||||
private val pushRuleTriggerListener: PushRuleTriggerListener,
|
private val pushRuleTriggerListener: PushRuleTriggerListener,
|
||||||
private val sessionListener: SessionListener,
|
private val sessionListener: SessionListener,
|
||||||
private val imageManager: ImageManager
|
private val imageManager: ImageManager
|
||||||
|
@ -52,7 +52,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService:
|
||||||
incomingVerificationRequestHandler.start(session)
|
incomingVerificationRequestHandler.start(session)
|
||||||
session.addListener(sessionListener)
|
session.addListener(sessionListener)
|
||||||
pushRuleTriggerListener.startWithSession(session)
|
pushRuleTriggerListener.startWithSession(session)
|
||||||
session.callSignalingService().addCallListener(webRtcPeerConnectionManager)
|
session.callSignalingService().addCallListener(callManager)
|
||||||
imageManager.onSessionStarted(session)
|
imageManager.onSessionStarted(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService:
|
||||||
// Do some cleanup first
|
// Do some cleanup first
|
||||||
getSafeActiveSession()?.let {
|
getSafeActiveSession()?.let {
|
||||||
Timber.w("clearActiveSession of ${it.myUserId}")
|
Timber.w("clearActiveSession of ${it.myUserId}")
|
||||||
it.callSignalingService().removeCallListener(webRtcPeerConnectionManager)
|
it.callSignalingService().removeCallListener(callManager)
|
||||||
it.removeListener(sessionListener)
|
it.removeListener(sessionListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import im.vector.app.features.auth.ReAuthActivity
|
||||||
import im.vector.app.features.call.CallControlsBottomSheet
|
import im.vector.app.features.call.CallControlsBottomSheet
|
||||||
import im.vector.app.features.call.VectorCallActivity
|
import im.vector.app.features.call.VectorCallActivity
|
||||||
import im.vector.app.features.call.conference.VectorJitsiActivity
|
import im.vector.app.features.call.conference.VectorJitsiActivity
|
||||||
|
import im.vector.app.features.call.transfer.CallTransferActivity
|
||||||
import im.vector.app.features.createdirect.CreateDirectRoomActivity
|
import im.vector.app.features.createdirect.CreateDirectRoomActivity
|
||||||
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||||
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
|
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
|
||||||
|
@ -146,6 +147,7 @@ interface ScreenComponent {
|
||||||
fun inject(activity: VectorJitsiActivity)
|
fun inject(activity: VectorJitsiActivity)
|
||||||
fun inject(activity: SearchActivity)
|
fun inject(activity: SearchActivity)
|
||||||
fun inject(activity: UserCodeActivity)
|
fun inject(activity: UserCodeActivity)
|
||||||
|
fun inject(activity: CallTransferActivity)
|
||||||
fun inject(activity: ReAuthActivity)
|
fun inject(activity: ReAuthActivity)
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
|
|
|
@ -29,7 +29,7 @@ import im.vector.app.core.error.ErrorFormatter
|
||||||
import im.vector.app.core.pushers.PushersManager
|
import im.vector.app.core.pushers.PushersManager
|
||||||
import im.vector.app.core.utils.AssetReader
|
import im.vector.app.core.utils.AssetReader
|
||||||
import im.vector.app.core.utils.DimensionConverter
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
import im.vector.app.features.configuration.VectorConfiguration
|
import im.vector.app.features.configuration.VectorConfiguration
|
||||||
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
|
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
|
||||||
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
|
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
|
||||||
|
@ -38,6 +38,7 @@ import im.vector.app.features.home.AvatarRenderer
|
||||||
import im.vector.app.features.home.HomeRoomListDataSource
|
import im.vector.app.features.home.HomeRoomListDataSource
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
|
import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||||
import im.vector.app.features.html.EventHtmlRenderer
|
import im.vector.app.features.html.EventHtmlRenderer
|
||||||
import im.vector.app.features.html.VectorHtmlCompressor
|
import im.vector.app.features.html.VectorHtmlCompressor
|
||||||
import im.vector.app.features.login.ReAuthHelper
|
import im.vector.app.features.login.ReAuthHelper
|
||||||
|
@ -156,7 +157,9 @@ interface VectorComponent {
|
||||||
|
|
||||||
fun pinLocker(): PinLocker
|
fun pinLocker(): PinLocker
|
||||||
|
|
||||||
fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager
|
fun webRtcCallManager(): WebRtcCallManager
|
||||||
|
|
||||||
|
fun roomSummaryHolder(): RoomSummariesHolder
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
|
@ -22,7 +22,7 @@ import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.multibindings.IntoMap
|
import dagger.multibindings.IntoMap
|
||||||
import im.vector.app.core.platform.ConfigurationViewModel
|
import im.vector.app.core.platform.ConfigurationViewModel
|
||||||
import im.vector.app.features.call.SharedActiveCallViewModel
|
import im.vector.app.features.call.SharedKnownCallsViewModel
|
||||||
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel
|
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel
|
||||||
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel
|
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel
|
||||||
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel
|
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel
|
||||||
|
@ -85,8 +85,8 @@ interface ViewModelModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@ViewModelKey(SharedActiveCallViewModel::class)
|
@ViewModelKey(SharedKnownCallsViewModel::class)
|
||||||
fun bindSharedActiveCallViewModel(viewModel: SharedActiveCallViewModel): ViewModel
|
fun bindSharedActiveCallViewModel(viewModel: SharedKnownCallsViewModel): ViewModel
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app.core.error
|
||||||
|
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.features.call.dialpad.DialPadLookup
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.MatrixError
|
import org.matrix.android.sdk.api.failure.MatrixError
|
||||||
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
||||||
|
@ -112,7 +113,9 @@ class DefaultErrorFormatter @Inject constructor(
|
||||||
throwable.localizedMessage
|
throwable.localizedMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> throwable.localizedMessage
|
is DialPadLookup.Failure ->
|
||||||
|
stringProvider.getString(R.string.call_dial_pad_lookup_error)
|
||||||
|
else -> throwable.localizedMessage
|
||||||
}
|
}
|
||||||
?: stringProvider.getString(R.string.unknown_error)
|
?: stringProvider.getString(R.string.unknown_error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,18 @@
|
||||||
package im.vector.app.core.extensions
|
package im.vector.app.core.extensions
|
||||||
|
|
||||||
// Create a new Set including the provided element if not already present, or removing the element if already present
|
// Create a new Set including the provided element if not already present, or removing the element if already present
|
||||||
fun <T> Set<T>.toggle(element: T): Set<T> {
|
fun <T> Set<T>.toggle(element: T, singleElement: Boolean = false): Set<T> {
|
||||||
return if (contains(element)) {
|
return if (contains(element)) {
|
||||||
minus(element)
|
if (singleElement) {
|
||||||
|
emptySet()
|
||||||
|
} else {
|
||||||
|
minus(element)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
plus(element)
|
if (singleElement) {
|
||||||
|
setOf(element)
|
||||||
|
} else {
|
||||||
|
plus(element)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,19 @@ package im.vector.app.core.extensions
|
||||||
|
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.text.style.ClickableSpan
|
||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
import android.text.style.UnderlineSpan
|
import android.text.style.UnderlineSpan
|
||||||
|
import android.view.View
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
@ -48,11 +56,13 @@ fun TextView.setTextOrHide(newText: CharSequence?, hideWhenBlank: Boolean = true
|
||||||
* @param coloredTextRes the resource id of the colored part of the text
|
* @param coloredTextRes the resource id of the colored part of the text
|
||||||
* @param colorAttribute attribute of the color. Default to colorAccent
|
* @param colorAttribute attribute of the color. Default to colorAccent
|
||||||
* @param underline true to also underline the text. Default to false
|
* @param underline true to also underline the text. Default to false
|
||||||
|
* @param onClick attributes to handle click on the colored part if needed
|
||||||
*/
|
*/
|
||||||
fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int,
|
fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int,
|
||||||
@StringRes coloredTextRes: Int,
|
@StringRes coloredTextRes: Int,
|
||||||
@AttrRes colorAttribute: Int = R.attr.colorAccent,
|
@AttrRes colorAttribute: Int = R.attr.colorAccent,
|
||||||
underline: Boolean = false) {
|
underline: Boolean = false,
|
||||||
|
onClick: (() -> Unit)?) {
|
||||||
val coloredPart = resources.getString(coloredTextRes)
|
val coloredPart = resources.getString(coloredTextRes)
|
||||||
// Insert colored part into the full text
|
// Insert colored part into the full text
|
||||||
val fullText = resources.getString(fullTextRes, coloredPart)
|
val fullText = resources.getString(fullTextRes, coloredPart)
|
||||||
|
@ -65,12 +75,38 @@ fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int,
|
||||||
text = SpannableString(fullText)
|
text = SpannableString(fullText)
|
||||||
.apply {
|
.apply {
|
||||||
setSpan(foregroundSpan, index, index + coloredPart.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
setSpan(foregroundSpan, index, index + coloredPart.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
if (onClick != null) {
|
||||||
|
val clickableSpan = object : ClickableSpan() {
|
||||||
|
override fun onClick(widget: View) {
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateDrawState(ds: TextPaint) {
|
||||||
|
ds.color = color
|
||||||
|
ds.isUnderlineText = !underline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSpan(clickableSpan, index, index + coloredPart.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
if (underline) {
|
if (underline) {
|
||||||
setSpan(UnderlineSpan(), index, index + coloredPart.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
setSpan(UnderlineSpan(), index, index + coloredPart.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TextView.setLeftDrawable(@DrawableRes iconRes: Int, @ColorRes tintColor: Int? = null) {
|
||||||
|
val icon = if (tintColor != null) {
|
||||||
|
val tint = ContextCompat.getColor(context, tintColor)
|
||||||
|
ContextCompat.getDrawable(context, iconRes)?.also {
|
||||||
|
DrawableCompat.setTint(it.mutate(), tint)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ContextCompat.getDrawable(context, iconRes)
|
||||||
|
}
|
||||||
|
setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set long click listener to copy the current text of the TextView to the clipboard and show a Snackbar
|
* Set long click listener to copy the current text of the TextView to the clipboard and show a Snackbar
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -45,6 +45,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.bumptech.glide.util.Util
|
import com.bumptech.glide.util.Util
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.jakewharton.rxbinding3.view.clicks
|
||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
|
@ -87,6 +88,7 @@ import io.reactivex.disposables.Disposable
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.failure.GlobalError
|
import org.matrix.android.sdk.api.failure.GlobalError
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScreenInjector {
|
abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScreenInjector {
|
||||||
|
@ -116,6 +118,18 @@ abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScr
|
||||||
.disposeOnDestroy()
|
.disposeOnDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Views
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
protected fun View.debouncedClicks(onClicked: () -> Unit) {
|
||||||
|
clicks()
|
||||||
|
.throttleFirst(300, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { onClicked() }
|
||||||
|
.disposeOnDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* DATA
|
* DATA
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
|
|
@ -16,33 +16,76 @@
|
||||||
|
|
||||||
package im.vector.app.core.services
|
package im.vector.app.core.services
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.Ringtone
|
|
||||||
import android.media.RingtoneManager
|
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.MediaPlayer
|
import android.media.MediaPlayer
|
||||||
|
import android.media.Ringtone
|
||||||
|
import android.media.RingtoneManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.VibrationEffect
|
||||||
|
import android.os.Vibrator
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.features.notifications.NotificationUtils
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class CallRingPlayerIncoming(
|
class CallRingPlayerIncoming(
|
||||||
context: Context
|
context: Context,
|
||||||
|
private val notificationUtils: NotificationUtils
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val applicationContext = context.applicationContext
|
private val applicationContext = context.applicationContext
|
||||||
private var r: Ringtone? = null
|
private var ringtone: Ringtone? = null
|
||||||
|
private var vibrator: Vibrator? = null
|
||||||
|
|
||||||
fun start() {
|
private val VIBRATE_PATTERN = longArrayOf(0, 400, 600)
|
||||||
val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
||||||
r = RingtoneManager.getRingtone(applicationContext, notification)
|
fun start(fromBg: Boolean) {
|
||||||
Timber.v("## VOIP Starting ringing incomming")
|
val audioManager = applicationContext.getSystemService<AudioManager>()
|
||||||
r?.play()
|
val incomingCallChannel = notificationUtils.getChannelForIncomingCall(fromBg)
|
||||||
|
val ringerMode = audioManager?.ringerMode
|
||||||
|
if (ringerMode == AudioManager.RINGER_MODE_NORMAL) {
|
||||||
|
playRingtoneIfNeeded(incomingCallChannel)
|
||||||
|
} else if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
|
||||||
|
vibrateIfNeeded(incomingCallChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playRingtoneIfNeeded(incomingCallChannel: NotificationChannel?) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && incomingCallChannel?.sound != null) {
|
||||||
|
Timber.v("Ringtone already configured by notification channel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
||||||
|
ringtone = RingtoneManager.getRingtone(applicationContext, ringtoneUri)
|
||||||
|
Timber.v("Play ringtone for incoming call")
|
||||||
|
ringtone?.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun vibrateIfNeeded(incomingCallChannel: NotificationChannel?) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && incomingCallChannel?.shouldVibrate().orFalse()) {
|
||||||
|
Timber.v("## Vibration already configured by notification channel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vibrator = applicationContext.getSystemService()
|
||||||
|
Timber.v("Vibrate for incoming call")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val vibrationEffect = VibrationEffect.createWaveform(VIBRATE_PATTERN, 0)
|
||||||
|
vibrator?.vibrate(vibrationEffect)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
vibrator?.vibrate(VIBRATE_PATTERN, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
r?.stop()
|
ringtone?.stop()
|
||||||
|
ringtone = null
|
||||||
|
vibrator?.cancel()
|
||||||
|
vibrator = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,12 +98,12 @@ class CallRingPlayerOutgoing(
|
||||||
private var player: MediaPlayer? = null
|
private var player: MediaPlayer? = null
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
val audioManager = applicationContext.getSystemService<AudioManager>()!!
|
val audioManager: AudioManager? = applicationContext.getSystemService()
|
||||||
player?.release()
|
player?.release()
|
||||||
player = createPlayer()
|
player = createPlayer()
|
||||||
|
|
||||||
// Check if sound is enabled
|
// Check if sound is enabled
|
||||||
val ringerMode = audioManager.ringerMode
|
val ringerMode = audioManager?.ringerMode
|
||||||
if (player != null && ringerMode == AudioManager.RINGER_MODE_NORMAL) {
|
if (player != null && ringerMode == AudioManager.RINGER_MODE_NORMAL) {
|
||||||
try {
|
try {
|
||||||
if (player?.isPlaying == false) {
|
if (player?.isPlaying == false) {
|
||||||
|
@ -89,14 +132,14 @@ class CallRingPlayerOutgoing(
|
||||||
|
|
||||||
mediaPlayer.setOnErrorListener(MediaPlayerErrorListener())
|
mediaPlayer.setOnErrorListener(MediaPlayerErrorListener())
|
||||||
mediaPlayer.isLooping = true
|
mediaPlayer.isLooping = true
|
||||||
if (Build.VERSION.SDK_INT <= 21) {
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING)
|
|
||||||
} else {
|
|
||||||
mediaPlayer.setAudioAttributes(AudioAttributes.Builder()
|
mediaPlayer.setAudioAttributes(AudioAttributes.Builder()
|
||||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||||
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||||
.build())
|
.build())
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING)
|
||||||
}
|
}
|
||||||
return mediaPlayer
|
return mediaPlayer
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
|
|
|
@ -22,30 +22,41 @@ import android.content.Intent
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.media.session.MediaButtonReceiver
|
import androidx.media.session.MediaButtonReceiver
|
||||||
|
import com.airbnb.mvrx.MvRx
|
||||||
import im.vector.app.core.extensions.vectorComponent
|
import im.vector.app.core.extensions.vectorComponent
|
||||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
import im.vector.app.features.call.CallArgs
|
||||||
|
import im.vector.app.features.call.VectorCallActivity
|
||||||
import im.vector.app.features.call.telecom.CallConnection
|
import im.vector.app.features.call.telecom.CallConnection
|
||||||
|
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||||
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import im.vector.app.features.notifications.NotificationUtils
|
import im.vector.app.features.notifications.NotificationUtils
|
||||||
|
import im.vector.app.features.popup.IncomingCallAlert
|
||||||
|
import im.vector.app.features.popup.PopupAlertManager
|
||||||
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Foreground service to manage calls
|
* Foreground service to manage calls
|
||||||
*/
|
*/
|
||||||
class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListener, BluetoothHeadsetReceiver.EventListener {
|
class CallService : VectorService() {
|
||||||
|
|
||||||
private val connections = mutableMapOf<String, CallConnection>()
|
private val connections = mutableMapOf<String, CallConnection>()
|
||||||
|
private val knownCalls = mutableSetOf<String>()
|
||||||
|
|
||||||
|
private lateinit var notificationManager: NotificationManagerCompat
|
||||||
private lateinit var notificationUtils: NotificationUtils
|
private lateinit var notificationUtils: NotificationUtils
|
||||||
private lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
private lateinit var callManager: WebRtcCallManager
|
||||||
|
private lateinit var avatarRenderer: AvatarRenderer
|
||||||
|
private lateinit var alertManager: PopupAlertManager
|
||||||
|
|
||||||
private var callRingPlayerIncoming: CallRingPlayerIncoming? = null
|
private var callRingPlayerIncoming: CallRingPlayerIncoming? = null
|
||||||
private var callRingPlayerOutgoing: CallRingPlayerOutgoing? = null
|
private var callRingPlayerOutgoing: CallRingPlayerOutgoing? = null
|
||||||
|
|
||||||
private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null
|
|
||||||
private var bluetoothHeadsetStateReceiver: BluetoothHeadsetReceiver? = null
|
|
||||||
|
|
||||||
// A media button receiver receives and helps translate hardware media playback buttons,
|
// A media button receiver receives and helps translate hardware media playback buttons,
|
||||||
// such as those found on wired and wireless headsets, into the appropriate callbacks in your app
|
// such as those found on wired and wireless headsets, into the appropriate callbacks in your app
|
||||||
private var mediaSession: MediaSessionCompat? = null
|
private var mediaSession: MediaSessionCompat? = null
|
||||||
|
@ -53,7 +64,7 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
||||||
override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
|
override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
|
||||||
val keyEvent = mediaButtonEvent?.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT) ?: return false
|
val keyEvent = mediaButtonEvent?.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT) ?: return false
|
||||||
if (keyEvent.keyCode == KeyEvent.KEYCODE_HEADSETHOOK) {
|
if (keyEvent.keyCode == KeyEvent.KEYCODE_HEADSETHOOK) {
|
||||||
webRtcPeerConnectionManager.headSetButtonTapped()
|
callManager.headSetButtonTapped()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -62,22 +73,19 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
notificationManager = NotificationManagerCompat.from(this)
|
||||||
notificationUtils = vectorComponent().notificationUtils()
|
notificationUtils = vectorComponent().notificationUtils()
|
||||||
webRtcPeerConnectionManager = vectorComponent().webRtcPeerConnectionManager()
|
callManager = vectorComponent().webRtcCallManager()
|
||||||
callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext)
|
avatarRenderer = vectorComponent().avatarRenderer()
|
||||||
|
alertManager = vectorComponent().alertManager()
|
||||||
|
callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext, notificationUtils)
|
||||||
callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext)
|
callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext)
|
||||||
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this)
|
|
||||||
bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(this, this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
callRingPlayerIncoming?.stop()
|
callRingPlayerIncoming?.stop()
|
||||||
callRingPlayerOutgoing?.stop()
|
callRingPlayerOutgoing?.stop()
|
||||||
wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(this, it) }
|
|
||||||
wiredHeadsetStateReceiver = null
|
|
||||||
bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(this, it) }
|
|
||||||
bluetoothHeadsetStateReceiver = null
|
|
||||||
mediaSession?.release()
|
mediaSession?.release()
|
||||||
mediaSession = null
|
mediaSession = null
|
||||||
}
|
}
|
||||||
|
@ -89,21 +97,17 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
||||||
setCallback(mediaSessionButtonCallback)
|
setCallback(mediaSessionButtonCallback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (intent == null) {
|
|
||||||
// Service started again by the system.
|
|
||||||
// TODO What do we do here?
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
mediaSession?.let {
|
mediaSession?.let {
|
||||||
// This ensures that the correct callbacks to MediaSessionCompat.Callback
|
// This ensures that the correct callbacks to MediaSessionCompat.Callback
|
||||||
// will be triggered based on the incoming KeyEvent.
|
// will be triggered based on the incoming KeyEvent.
|
||||||
MediaButtonReceiver.handleIntent(it, intent)
|
MediaButtonReceiver.handleIntent(it, intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (intent.action) {
|
when (intent?.action) {
|
||||||
ACTION_INCOMING_RINGING_CALL -> {
|
ACTION_INCOMING_RINGING_CALL -> {
|
||||||
mediaSession?.isActive = true
|
mediaSession?.isActive = true
|
||||||
callRingPlayerIncoming?.start()
|
val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false)
|
||||||
|
callRingPlayerIncoming?.start(fromBg)
|
||||||
displayIncomingCallNotification(intent)
|
displayIncomingCallNotification(intent)
|
||||||
}
|
}
|
||||||
ACTION_OUTGOING_RINGING_CALL -> {
|
ACTION_OUTGOING_RINGING_CALL -> {
|
||||||
|
@ -111,33 +115,28 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
||||||
callRingPlayerOutgoing?.start()
|
callRingPlayerOutgoing?.start()
|
||||||
displayOutgoingRingingCallNotification(intent)
|
displayOutgoingRingingCallNotification(intent)
|
||||||
}
|
}
|
||||||
ACTION_ONGOING_CALL -> {
|
ACTION_ONGOING_CALL -> {
|
||||||
callRingPlayerIncoming?.stop()
|
callRingPlayerIncoming?.stop()
|
||||||
callRingPlayerOutgoing?.stop()
|
callRingPlayerOutgoing?.stop()
|
||||||
displayCallInProgressNotification(intent)
|
displayCallInProgressNotification(intent)
|
||||||
}
|
}
|
||||||
ACTION_NO_ACTIVE_CALL -> hideCallNotifications()
|
ACTION_CALL_CONNECTING -> {
|
||||||
ACTION_CALL_CONNECTING -> {
|
|
||||||
// lower notification priority
|
// lower notification priority
|
||||||
displayCallInProgressNotification(intent)
|
displayCallInProgressNotification(intent)
|
||||||
// stop ringing
|
// stop ringing
|
||||||
callRingPlayerIncoming?.stop()
|
callRingPlayerIncoming?.stop()
|
||||||
callRingPlayerOutgoing?.stop()
|
callRingPlayerOutgoing?.stop()
|
||||||
}
|
}
|
||||||
ACTION_ONGOING_CALL_BG -> {
|
ACTION_CALL_TERMINATED -> {
|
||||||
// there is an ongoing call but call activity is in background
|
handleCallTerminated(intent)
|
||||||
displayCallOnGoingInBackground(intent)
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Should not happen
|
handleUnexpectedState(null)
|
||||||
callRingPlayerIncoming?.stop()
|
|
||||||
callRingPlayerOutgoing?.stop()
|
|
||||||
myStopSelf()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want the system to restore the service if killed
|
// We want the system to restore the service if killed
|
||||||
return START_STICKY
|
return START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================================
|
// ================================================================================
|
||||||
|
@ -147,64 +146,90 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
||||||
/**
|
/**
|
||||||
* Display a permanent notification when there is an incoming call.
|
* Display a permanent notification when there is an incoming call.
|
||||||
*
|
*
|
||||||
* @param session the session
|
|
||||||
* @param isVideo true if this is a video call, false for voice call
|
|
||||||
* @param room the room
|
|
||||||
* @param callId the callId
|
|
||||||
*/
|
*/
|
||||||
private fun displayIncomingCallNotification(intent: Intent) {
|
private fun displayIncomingCallNotification(intent: Intent) {
|
||||||
Timber.v("## VOIP displayIncomingCallNotification $intent")
|
Timber.v("## VOIP displayIncomingCallNotification $intent")
|
||||||
|
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||||
// the incoming call in progress is already displayed
|
val call = callManager.getCallById(callId) ?: return Unit.also {
|
||||||
// if (!TextUtils.isEmpty(mIncomingCallId)) {
|
handleUnexpectedState(callId)
|
||||||
// Timber.v("displayIncomingCallNotification : the incoming call in progress is already displayed")
|
}
|
||||||
// } else if (!TextUtils.isEmpty(mCallIdInProgress)) {
|
val isVideoCall = call.mxCall.isVideoCall
|
||||||
// Timber.v("displayIncomingCallNotification : a 'call in progress' notification is displayed")
|
val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false)
|
||||||
// } else
|
val opponentMatrixItem = getOpponentMatrixItem(call)
|
||||||
// // if (null == webRtcPeerConnectionManager.currentCall)
|
|
||||||
// {
|
|
||||||
val callId = intent.getStringExtra(EXTRA_CALL_ID)
|
|
||||||
|
|
||||||
Timber.v("displayIncomingCallNotification : display the dedicated notification")
|
Timber.v("displayIncomingCallNotification : display the dedicated notification")
|
||||||
|
val incomingCallAlert = IncomingCallAlert(callId,
|
||||||
|
shouldBeDisplayedIn = { activity ->
|
||||||
|
if (activity is VectorCallActivity) {
|
||||||
|
activity.intent.getParcelableExtra<CallArgs>(MvRx.KEY_ARG)?.callId != call.callId
|
||||||
|
} else true
|
||||||
|
}
|
||||||
|
).apply {
|
||||||
|
viewBinder = IncomingCallAlert.ViewBinder(
|
||||||
|
matrixItem = opponentMatrixItem,
|
||||||
|
avatarRenderer = avatarRenderer,
|
||||||
|
isVideoCall = isVideoCall,
|
||||||
|
onAccept = { showCallScreen(call, VectorCallActivity.INCOMING_ACCEPT) },
|
||||||
|
onReject = { call.endCall() }
|
||||||
|
)
|
||||||
|
dismissedAction = Runnable { call.endCall() }
|
||||||
|
contentAction = Runnable { showCallScreen(call, VectorCallActivity.INCOMING_RINGING) }
|
||||||
|
}
|
||||||
|
alertManager.postVectorAlert(incomingCallAlert)
|
||||||
val notification = notificationUtils.buildIncomingCallNotification(
|
val notification = notificationUtils.buildIncomingCallNotification(
|
||||||
intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
|
mxCall = call.mxCall,
|
||||||
intent.getStringExtra(EXTRA_ROOM_NAME) ?: "",
|
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId,
|
||||||
intent.getStringExtra(EXTRA_ROOM_ID) ?: "",
|
fromBg = fromBg
|
||||||
callId ?: "")
|
)
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
if (knownCalls.isEmpty()) {
|
||||||
|
startForeground(callId.hashCode(), notification)
|
||||||
|
} else {
|
||||||
|
notificationManager.notify(callId.hashCode(), notification)
|
||||||
|
}
|
||||||
|
knownCalls.add(callId)
|
||||||
|
}
|
||||||
|
|
||||||
// mIncomingCallId = callId
|
private fun handleCallTerminated(intent: Intent) {
|
||||||
|
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||||
|
alertManager.cancelAlert(callId)
|
||||||
|
if (!knownCalls.remove(callId)) {
|
||||||
|
Timber.v("Call terminated for unknown call $callId$")
|
||||||
|
handleUnexpectedState(callId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val notification = notificationUtils.buildCallEndedNotification()
|
||||||
|
notificationManager.notify(callId.hashCode(), notification)
|
||||||
|
if (knownCalls.isEmpty()) {
|
||||||
|
mediaSession?.isActive = false
|
||||||
|
myStopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// turn the screen on for 3 seconds
|
private fun showCallScreen(call: WebRtcCall, mode: String) {
|
||||||
// if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) {
|
val intent = VectorCallActivity.newIntent(
|
||||||
// try {
|
context = this,
|
||||||
// val pm = getSystemService<PowerManager>()!!
|
mxCall = call.mxCall,
|
||||||
// val wl = pm.newWakeLock(
|
mode = mode
|
||||||
// WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
)
|
||||||
// CallService::class.java.simpleName)
|
startActivity(intent)
|
||||||
// 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")
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun displayOutgoingRingingCallNotification(intent: Intent) {
|
private fun displayOutgoingRingingCallNotification(intent: Intent) {
|
||||||
val callId = intent.getStringExtra(EXTRA_CALL_ID)
|
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||||
|
val call = callManager.getCallById(callId) ?: return Unit.also {
|
||||||
|
handleUnexpectedState(callId)
|
||||||
|
}
|
||||||
|
val opponentMatrixItem = getOpponentMatrixItem(call)
|
||||||
Timber.v("displayOutgoingCallNotification : display the dedicated notification")
|
Timber.v("displayOutgoingCallNotification : display the dedicated notification")
|
||||||
val notification = notificationUtils.buildOutgoingRingingCallNotification(
|
val notification = notificationUtils.buildOutgoingRingingCallNotification(
|
||||||
intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
|
mxCall = call.mxCall,
|
||||||
intent.getStringExtra(EXTRA_ROOM_NAME) ?: "",
|
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId
|
||||||
intent.getStringExtra(EXTRA_ROOM_ID) ?: "",
|
)
|
||||||
callId ?: "")
|
if (knownCalls.isEmpty()) {
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
startForeground(callId.hashCode(), notification)
|
||||||
|
} else {
|
||||||
|
notificationManager.notify(callId.hashCode(), notification)
|
||||||
|
}
|
||||||
|
knownCalls.add(callId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -213,125 +238,78 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
||||||
private fun displayCallInProgressNotification(intent: Intent) {
|
private fun displayCallInProgressNotification(intent: Intent) {
|
||||||
Timber.v("## VOIP displayCallInProgressNotification")
|
Timber.v("## VOIP displayCallInProgressNotification")
|
||||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||||
|
val call = callManager.getCallById(callId) ?: return Unit.also {
|
||||||
|
handleUnexpectedState(callId)
|
||||||
|
}
|
||||||
|
val opponentMatrixItem = getOpponentMatrixItem(call)
|
||||||
|
alertManager.cancelAlert(callId)
|
||||||
val notification = notificationUtils.buildPendingCallNotification(
|
val notification = notificationUtils.buildPendingCallNotification(
|
||||||
intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
|
mxCall = call.mxCall,
|
||||||
intent.getStringExtra(EXTRA_ROOM_NAME) ?: "",
|
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId
|
||||||
intent.getStringExtra(EXTRA_ROOM_ID) ?: "",
|
)
|
||||||
intent.getStringExtra(EXTRA_MATRIX_ID) ?: "",
|
if (knownCalls.isEmpty()) {
|
||||||
callId)
|
startForeground(callId.hashCode(), notification)
|
||||||
|
} else {
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
notificationManager.notify(callId.hashCode(), notification)
|
||||||
|
}
|
||||||
// mCallIdInProgress = callId
|
knownCalls.add(callId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun handleUnexpectedState(callId: String?) {
|
||||||
* Display a call in progress notification.
|
Timber.v("Fallback to clear everything")
|
||||||
*/
|
callRingPlayerIncoming?.stop()
|
||||||
private fun displayCallOnGoingInBackground(intent: Intent) {
|
callRingPlayerOutgoing?.stop()
|
||||||
Timber.v("## VOIP displayCallInProgressNotification")
|
if (callId != null) {
|
||||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
notificationManager.cancel(callId.hashCode())
|
||||||
|
}
|
||||||
val notification = notificationUtils.buildPendingCallNotification(
|
|
||||||
isVideo = intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
|
|
||||||
roomName = intent.getStringExtra(EXTRA_ROOM_NAME) ?: "",
|
|
||||||
roomId = intent.getStringExtra(EXTRA_ROOM_ID) ?: "",
|
|
||||||
matrixId = intent.getStringExtra(EXTRA_MATRIX_ID) ?: "",
|
|
||||||
callId = callId,
|
|
||||||
fromBg = true)
|
|
||||||
|
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
|
||||||
|
|
||||||
// mCallIdInProgress = callId
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide the permanent call notifications
|
|
||||||
*/
|
|
||||||
private fun hideCallNotifications() {
|
|
||||||
val notification = notificationUtils.buildCallEndedNotification()
|
val notification = notificationUtils.buildCallEndedNotification()
|
||||||
|
startForeground(DEFAULT_NOTIFICATION_ID, notification)
|
||||||
mediaSession?.isActive = false
|
if (knownCalls.isEmpty()) {
|
||||||
// It's mandatory to startForeground to avoid crash
|
mediaSession?.isActive = false
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
myStopSelf()
|
||||||
|
}
|
||||||
myStopSelf()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addConnection(callConnection: CallConnection) {
|
fun addConnection(callConnection: CallConnection) {
|
||||||
connections[callConnection.callId] = callConnection
|
connections[callConnection.callId] = callConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getOpponentMatrixItem(call: WebRtcCall): MatrixItem? {
|
||||||
|
return vectorComponent().currentSession().getUser(call.mxCall.opponentUserId)?.toMatrixItem()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val NOTIFICATION_ID = 6480
|
private const val DEFAULT_NOTIFICATION_ID = 6480
|
||||||
|
|
||||||
private const val ACTION_INCOMING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_INCOMING_RINGING_CALL"
|
private const val ACTION_INCOMING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_INCOMING_RINGING_CALL"
|
||||||
private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_OUTGOING_RINGING_CALL"
|
private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_OUTGOING_RINGING_CALL"
|
||||||
private const val ACTION_CALL_CONNECTING = "im.vector.app.core.services.CallService.ACTION_CALL_CONNECTING"
|
private const val ACTION_CALL_CONNECTING = "im.vector.app.core.services.CallService.ACTION_CALL_CONNECTING"
|
||||||
private const val ACTION_ONGOING_CALL = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL"
|
private const val ACTION_ONGOING_CALL = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL"
|
||||||
private const val ACTION_ONGOING_CALL_BG = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL_BG"
|
private const val ACTION_CALL_TERMINATED = "im.vector.app.core.services.CallService.ACTION_CALL_TERMINATED"
|
||||||
private const val ACTION_NO_ACTIVE_CALL = "im.vector.app.core.services.CallService.NO_ACTIVE_CALL"
|
private const val ACTION_NO_ACTIVE_CALL = "im.vector.app.core.services.CallService.NO_ACTIVE_CALL"
|
||||||
// private const val ACTION_ACTIVITY_VISIBLE = "im.vector.app.core.services.CallService.ACTION_ACTIVITY_VISIBLE"
|
// private const val ACTION_ACTIVITY_VISIBLE = "im.vector.app.core.services.CallService.ACTION_ACTIVITY_VISIBLE"
|
||||||
// private const val ACTION_STOP_RINGING = "im.vector.app.core.services.CallService.ACTION_STOP_RINGING"
|
// private const val ACTION_STOP_RINGING = "im.vector.app.core.services.CallService.ACTION_STOP_RINGING"
|
||||||
|
|
||||||
private const val EXTRA_IS_VIDEO = "EXTRA_IS_VIDEO"
|
|
||||||
private const val EXTRA_ROOM_NAME = "EXTRA_ROOM_NAME"
|
|
||||||
private const val EXTRA_ROOM_ID = "EXTRA_ROOM_ID"
|
|
||||||
private const val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID"
|
|
||||||
private const val EXTRA_CALL_ID = "EXTRA_CALL_ID"
|
private const val EXTRA_CALL_ID = "EXTRA_CALL_ID"
|
||||||
|
private const val EXTRA_IS_IN_BG = "EXTRA_IS_IN_BG"
|
||||||
|
|
||||||
fun onIncomingCallRinging(context: Context,
|
fun onIncomingCallRinging(context: Context,
|
||||||
isVideo: Boolean,
|
callId: String,
|
||||||
roomName: String,
|
isInBackground: Boolean) {
|
||||||
roomId: String,
|
|
||||||
matrixId: String,
|
|
||||||
callId: String) {
|
|
||||||
val intent = Intent(context, CallService::class.java)
|
val intent = Intent(context, CallService::class.java)
|
||||||
.apply {
|
.apply {
|
||||||
action = ACTION_INCOMING_RINGING_CALL
|
action = ACTION_INCOMING_RINGING_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)
|
putExtra(EXTRA_CALL_ID, callId)
|
||||||
|
putExtra(EXTRA_IS_IN_BG, isInBackground)
|
||||||
}
|
}
|
||||||
|
|
||||||
ContextCompat.startForegroundService(context, intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onOnGoingCallBackground(context: Context,
|
|
||||||
isVideo: Boolean,
|
|
||||||
roomName: String,
|
|
||||||
roomId: String,
|
|
||||||
matrixId: String,
|
|
||||||
callId: String) {
|
|
||||||
val intent = Intent(context, CallService::class.java)
|
|
||||||
.apply {
|
|
||||||
action = ACTION_ONGOING_CALL_BG
|
|
||||||
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)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onOutgoingCallRinging(context: Context,
|
fun onOutgoingCallRinging(context: Context,
|
||||||
isVideo: Boolean,
|
|
||||||
roomName: String,
|
|
||||||
roomId: String,
|
|
||||||
matrixId: String,
|
|
||||||
callId: String) {
|
callId: String) {
|
||||||
val intent = Intent(context, CallService::class.java)
|
val intent = Intent(context, CallService::class.java)
|
||||||
.apply {
|
.apply {
|
||||||
action = ACTION_OUTGOING_RINGING_CALL
|
action = ACTION_OUTGOING_RINGING_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)
|
putExtra(EXTRA_CALL_ID, callId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,30 +317,22 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPendingCall(context: Context,
|
fun onPendingCall(context: Context,
|
||||||
isVideo: Boolean,
|
|
||||||
roomName: String,
|
|
||||||
roomId: String,
|
|
||||||
matrixId: String,
|
|
||||||
callId: String) {
|
callId: String) {
|
||||||
val intent = Intent(context, CallService::class.java)
|
val intent = Intent(context, CallService::class.java)
|
||||||
.apply {
|
.apply {
|
||||||
action = ACTION_ONGOING_CALL
|
action = ACTION_ONGOING_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)
|
putExtra(EXTRA_CALL_ID, callId)
|
||||||
}
|
}
|
||||||
|
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onNoActiveCall(context: Context) {
|
fun onCallTerminated(context: Context, callId: String) {
|
||||||
val intent = Intent(context, CallService::class.java)
|
val intent = Intent(context, CallService::class.java)
|
||||||
.apply {
|
.apply {
|
||||||
action = ACTION_NO_ACTIVE_CALL
|
action = ACTION_CALL_TERMINATED
|
||||||
|
putExtra(EXTRA_CALL_ID, callId)
|
||||||
}
|
}
|
||||||
|
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -372,14 +342,4 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
||||||
return this@CallService
|
return this@CallService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
|
|
||||||
Timber.v("## VOIP: onHeadsetEvent $event")
|
|
||||||
webRtcPeerConnectionManager.onWiredDeviceEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
|
|
||||||
Timber.v("## VOIP: onBTHeadsetEvent $event")
|
|
||||||
webRtcPeerConnectionManager.onWirelessDeviceEvent(event)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,13 +41,13 @@ class BottomSheetActionButton @JvmOverloads constructor(
|
||||||
var title: String? = null
|
var title: String? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
views.itemVerificationActionTitle.setTextOrHide(value)
|
views.bottomSheetActionTitle.setTextOrHide(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var subTitle: String? = null
|
var subTitle: String? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
views.itemVerificationActionSubTitle.setTextOrHide(value)
|
views.bottomSheetActionSubTitle.setTextOrHide(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var forceStartPadding: Boolean? = null
|
var forceStartPadding: Boolean? = null
|
||||||
|
@ -55,9 +55,9 @@ class BottomSheetActionButton @JvmOverloads constructor(
|
||||||
field = value
|
field = value
|
||||||
if (leftIcon == null) {
|
if (leftIcon == null) {
|
||||||
if (forceStartPadding == true) {
|
if (forceStartPadding == true) {
|
||||||
views.itemVerificationLeftIcon.isInvisible = true
|
views.bottomSheetActionLeftIcon.isInvisible = true
|
||||||
} else {
|
} else {
|
||||||
views.itemVerificationLeftIcon.isGone = true
|
views.bottomSheetActionLeftIcon.isGone = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,33 +67,33 @@ class BottomSheetActionButton @JvmOverloads constructor(
|
||||||
field = value
|
field = value
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
if (forceStartPadding == true) {
|
if (forceStartPadding == true) {
|
||||||
views.itemVerificationLeftIcon.isInvisible = true
|
views.bottomSheetActionLeftIcon.isInvisible = true
|
||||||
} else {
|
} else {
|
||||||
views.itemVerificationLeftIcon.isGone = true
|
views.bottomSheetActionLeftIcon.isGone = true
|
||||||
}
|
}
|
||||||
views.itemVerificationLeftIcon.setImageDrawable(null)
|
views.bottomSheetActionLeftIcon.setImageDrawable(null)
|
||||||
} else {
|
} else {
|
||||||
views.itemVerificationLeftIcon.isVisible = true
|
views.bottomSheetActionLeftIcon.isVisible = true
|
||||||
views.itemVerificationLeftIcon.setImageDrawable(value)
|
views.bottomSheetActionLeftIcon.setImageDrawable(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var rightIcon: Drawable? = null
|
var rightIcon: Drawable? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
views.itemVerificationActionIcon.setImageDrawable(value)
|
views.bottomSheetActionIcon.setImageDrawable(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var tint: Int? = null
|
var tint: Int? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
views.itemVerificationLeftIcon.imageTintList = value?.let { ColorStateList.valueOf(value) }
|
views.bottomSheetActionLeftIcon.imageTintList = value?.let { ColorStateList.valueOf(value) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var titleTextColor: Int? = null
|
var titleTextColor: Int? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
value?.let { views.itemVerificationActionTitle.setTextColor(it) }
|
value?.let { views.bottomSheetActionTitle.setTextColor(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
|
@ -20,9 +20,12 @@ import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.databinding.ViewCurrentCallsBinding
|
||||||
|
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
|
import org.matrix.android.sdk.api.session.call.CallState
|
||||||
|
|
||||||
class ActiveCallView @JvmOverloads constructor(
|
class CurrentCallsView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = 0
|
defStyleAttr: Int = 0
|
||||||
|
@ -32,15 +35,32 @@ class ActiveCallView @JvmOverloads constructor(
|
||||||
fun onTapToReturnToCall()
|
fun onTapToReturnToCall()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val views: ViewCurrentCallsBinding
|
||||||
var callback: Callback? = null
|
var callback: Callback? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setupView()
|
inflate(context, R.layout.view_current_calls, this)
|
||||||
}
|
views = ViewCurrentCallsBinding.bind(this)
|
||||||
|
|
||||||
private fun setupView() {
|
|
||||||
inflate(context, R.layout.view_active_call_view, this)
|
|
||||||
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary))
|
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary))
|
||||||
setOnClickListener { callback?.onTapToReturnToCall() }
|
setOnClickListener { callback?.onTapToReturnToCall() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun render(calls: List<WebRtcCall>, formattedDuration: String) {
|
||||||
|
val connectedCalls = calls.filter {
|
||||||
|
it.mxCall.state is CallState.Connected
|
||||||
|
}
|
||||||
|
val heldCalls = connectedCalls.filter {
|
||||||
|
it.isLocalOnHold || it.remoteOnHold
|
||||||
|
}
|
||||||
|
if (connectedCalls.isEmpty()) return
|
||||||
|
views.currentCallsInfo.text = if (connectedCalls.size == heldCalls.size) {
|
||||||
|
resources.getQuantityString(R.plurals.call_only_paused, heldCalls.size, heldCalls.size)
|
||||||
|
} else {
|
||||||
|
if (heldCalls.isEmpty()) {
|
||||||
|
resources.getString(R.string.call_only_active, formattedDuration)
|
||||||
|
} else {
|
||||||
|
resources.getQuantityString(R.plurals.call_one_active_and_other_paused, heldCalls.size, formattedDuration, heldCalls.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -16,42 +16,56 @@
|
||||||
|
|
||||||
package im.vector.app.core.ui.views
|
package im.vector.app.core.ui.views
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import im.vector.app.core.utils.DebouncedClickListener
|
import im.vector.app.core.utils.DebouncedClickListener
|
||||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
import org.matrix.android.sdk.api.session.call.CallState
|
||||||
import org.matrix.android.sdk.api.session.call.EglUtils
|
import im.vector.app.features.call.utils.EglUtils
|
||||||
import org.matrix.android.sdk.api.session.call.MxCall
|
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||||
import org.webrtc.RendererCommon
|
import org.webrtc.RendererCommon
|
||||||
import org.webrtc.SurfaceViewRenderer
|
import org.webrtc.SurfaceViewRenderer
|
||||||
|
|
||||||
class ActiveCallViewHolder {
|
class KnownCallsViewHolder {
|
||||||
|
|
||||||
private var activeCallPiP: SurfaceViewRenderer? = null
|
private var activeCallPiP: SurfaceViewRenderer? = null
|
||||||
private var activeCallView: ActiveCallView? = null
|
private var currentCallsView: CurrentCallsView? = null
|
||||||
private var pipWrapper: CardView? = null
|
private var pipWrapper: CardView? = null
|
||||||
|
private var currentCall: WebRtcCall? = null
|
||||||
|
private var calls: List<WebRtcCall> = emptyList()
|
||||||
|
|
||||||
private var activeCallPipInitialized = false
|
private var activeCallPipInitialized = false
|
||||||
|
|
||||||
fun updateCall(activeCall: MxCall?, webRtcPeerConnectionManager: WebRtcPeerConnectionManager) {
|
private val tickListener = object : WebRtcCall.Listener {
|
||||||
val hasActiveCall = activeCall?.state is CallState.Connected
|
override fun onTick(formattedDuration: String) {
|
||||||
|
currentCallsView?.render(calls, formattedDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCall(currentCall: WebRtcCall?, calls: List<WebRtcCall>) {
|
||||||
|
activeCallPiP?.let {
|
||||||
|
this.currentCall?.detachRenderers(listOf(it))
|
||||||
|
}
|
||||||
|
this.currentCall?.removeListener(tickListener)
|
||||||
|
this.currentCall = currentCall
|
||||||
|
this.currentCall?.addListener(tickListener)
|
||||||
|
this.calls = calls
|
||||||
|
val hasActiveCall = currentCall?.mxCall?.state is CallState.Connected
|
||||||
if (hasActiveCall) {
|
if (hasActiveCall) {
|
||||||
val isVideoCall = activeCall?.isVideoCall == true
|
val isVideoCall = currentCall?.mxCall?.isVideoCall == true
|
||||||
if (isVideoCall) initIfNeeded()
|
if (isVideoCall) initIfNeeded()
|
||||||
activeCallView?.isVisible = !isVideoCall
|
currentCallsView?.isVisible = !isVideoCall
|
||||||
|
currentCallsView?.render(calls, currentCall?.formattedDuration() ?: "")
|
||||||
pipWrapper?.isVisible = isVideoCall
|
pipWrapper?.isVisible = isVideoCall
|
||||||
activeCallPiP?.isVisible = isVideoCall
|
activeCallPiP?.isVisible = isVideoCall
|
||||||
activeCallPiP?.let {
|
activeCallPiP?.let {
|
||||||
webRtcPeerConnectionManager.attachViewRenderers(null, it, null)
|
currentCall?.attachViewRenderers(null, it, null)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
activeCallView?.isVisible = false
|
currentCallsView?.isVisible = false
|
||||||
activeCallPiP?.isVisible = false
|
activeCallPiP?.isVisible = false
|
||||||
pipWrapper?.isVisible = false
|
pipWrapper?.isVisible = false
|
||||||
activeCallPiP?.let {
|
activeCallPiP?.let {
|
||||||
webRtcPeerConnectionManager.detachRenderers(listOf(it))
|
currentCall?.detachRenderers(listOf(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,30 +83,31 @@ class ActiveCallViewHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(activeCallPiP: SurfaceViewRenderer, activeCallView: ActiveCallView, pipWrapper: CardView, interactionListener: ActiveCallView.Callback) {
|
fun bind(activeCallPiP: SurfaceViewRenderer, activeCallView: CurrentCallsView, pipWrapper: CardView, interactionListener: CurrentCallsView.Callback) {
|
||||||
this.activeCallPiP = activeCallPiP
|
this.activeCallPiP = activeCallPiP
|
||||||
this.activeCallView = activeCallView
|
this.currentCallsView = activeCallView
|
||||||
this.pipWrapper = pipWrapper
|
this.pipWrapper = pipWrapper
|
||||||
|
this.currentCallsView?.callback = interactionListener
|
||||||
this.activeCallView?.callback = interactionListener
|
|
||||||
pipWrapper.setOnClickListener(
|
pipWrapper.setOnClickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { _ ->
|
DebouncedClickListener({ _ ->
|
||||||
interactionListener.onTapToReturnToCall()
|
interactionListener.onTapToReturnToCall()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
this.currentCall?.addListener(tickListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unBind(webRtcPeerConnectionManager: WebRtcPeerConnectionManager) {
|
fun unBind() {
|
||||||
activeCallPiP?.let {
|
activeCallPiP?.let {
|
||||||
webRtcPeerConnectionManager.detachRenderers(listOf(it))
|
currentCall?.detachRenderers(listOf(it))
|
||||||
}
|
}
|
||||||
if (activeCallPipInitialized) {
|
if (activeCallPipInitialized) {
|
||||||
activeCallPiP?.release()
|
activeCallPiP?.release()
|
||||||
}
|
}
|
||||||
this.activeCallView?.callback = null
|
this.currentCallsView?.callback = null
|
||||||
|
this.currentCall?.removeListener(tickListener)
|
||||||
pipWrapper?.setOnClickListener(null)
|
pipWrapper?.setOnClickListener(null)
|
||||||
activeCallPiP = null
|
activeCallPiP = null
|
||||||
activeCallView = null
|
currentCallsView = null
|
||||||
pipWrapper = null
|
pipWrapper = null
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright 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.app.core.utils
|
||||||
|
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
|
||||||
|
class CountUpTimer(private val intervalInMs: Long) {
|
||||||
|
|
||||||
|
private val elapsedTime: AtomicLong = AtomicLong()
|
||||||
|
private val resumed: AtomicBoolean = AtomicBoolean(false)
|
||||||
|
|
||||||
|
private val disposable = Observable.interval(intervalInMs, TimeUnit.MILLISECONDS)
|
||||||
|
.filter { _ -> resumed.get() }
|
||||||
|
.doOnNext { _ -> elapsedTime.addAndGet(intervalInMs) }
|
||||||
|
.subscribe {
|
||||||
|
tickListener?.onTick(elapsedTime.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
var tickListener: TickListener? = null
|
||||||
|
|
||||||
|
fun elapsedTime(): Long {
|
||||||
|
return elapsedTime.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pause() {
|
||||||
|
resumed.set(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resume() {
|
||||||
|
resumed.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
disposable.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TickListener {
|
||||||
|
fun onTick(milliseconds: Long)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,318 +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.app.features.call
|
|
||||||
|
|
||||||
import android.bluetooth.BluetoothAdapter
|
|
||||||
import android.bluetooth.BluetoothManager
|
|
||||||
import android.bluetooth.BluetoothProfile
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.media.AudioManager
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import im.vector.app.core.services.WiredHeadsetStateReceiver
|
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
|
||||||
import org.matrix.android.sdk.api.session.call.MxCall
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
class CallAudioManager(
|
|
||||||
val applicationContext: Context,
|
|
||||||
val configChange: (() -> Unit)?
|
|
||||||
) {
|
|
||||||
|
|
||||||
enum class SoundDevice {
|
|
||||||
PHONE,
|
|
||||||
SPEAKER,
|
|
||||||
HEADSET,
|
|
||||||
WIRELESS_HEADSET
|
|
||||||
}
|
|
||||||
|
|
||||||
// if all calls to audio manager not in the same thread it's not working well.
|
|
||||||
private val executor = Executors.newSingleThreadExecutor()
|
|
||||||
|
|
||||||
private var audioManager: AudioManager? = null
|
|
||||||
|
|
||||||
private var savedIsSpeakerPhoneOn = false
|
|
||||||
private var savedIsMicrophoneMute = false
|
|
||||||
private var savedAudioMode = AudioManager.MODE_NORMAL
|
|
||||||
|
|
||||||
private var connectedBlueToothHeadset: BluetoothProfile? = null
|
|
||||||
private var wantsBluetoothConnection = false
|
|
||||||
|
|
||||||
private var bluetoothAdapter: BluetoothAdapter? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
executor.execute {
|
|
||||||
audioManager = applicationContext.getSystemService()
|
|
||||||
}
|
|
||||||
val bm = applicationContext.getSystemService<BluetoothManager>()
|
|
||||||
val adapter = bm?.adapter
|
|
||||||
Timber.d("## VOIP Bluetooth adapter $adapter")
|
|
||||||
bluetoothAdapter = adapter
|
|
||||||
adapter?.getProfileProxy(applicationContext, object : BluetoothProfile.ServiceListener {
|
|
||||||
override fun onServiceDisconnected(profile: Int) {
|
|
||||||
Timber.d("## VOIP onServiceDisconnected $profile")
|
|
||||||
if (profile == BluetoothProfile.HEADSET) {
|
|
||||||
connectedBlueToothHeadset = null
|
|
||||||
configChange?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
|
|
||||||
Timber.d("## VOIP onServiceConnected $profile , proxy:$proxy")
|
|
||||||
if (profile == BluetoothProfile.HEADSET) {
|
|
||||||
connectedBlueToothHeadset = proxy
|
|
||||||
configChange?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, BluetoothProfile.HEADSET)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
|
||||||
|
|
||||||
// Called on the listener to notify if the audio focus for this listener has been changed.
|
|
||||||
// The |focusChange| value indicates whether the focus was gained, whether the focus was lost,
|
|
||||||
// and whether that loss is transient, or whether the new focus holder will hold it for an
|
|
||||||
// unknown amount of time.
|
|
||||||
Timber.v("## VOIP: Audio focus change $focusChange")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startForCall(mxCall: MxCall) {
|
|
||||||
Timber.v("## VOIP: AudioManager startForCall ${mxCall.callId}")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupAudioManager(mxCall: MxCall) {
|
|
||||||
Timber.v("## VOIP: AudioManager setupAudioManager ${mxCall.callId}")
|
|
||||||
val audioManager = audioManager ?: return
|
|
||||||
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn
|
|
||||||
savedIsMicrophoneMute = audioManager.isMicrophoneMute
|
|
||||||
savedAudioMode = audioManager.mode
|
|
||||||
|
|
||||||
// Request audio playout focus (without ducking) and install listener for changes in focus.
|
|
||||||
|
|
||||||
// Remove the deprecation forces us to use 2 different method depending on API level
|
|
||||||
@Suppress("DEPRECATION") val result = audioManager.requestAudioFocus(audioFocusChangeListener,
|
|
||||||
AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
|
|
||||||
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
|
||||||
Timber.d("## VOIP Audio focus request granted for VOICE_CALL streams")
|
|
||||||
} else {
|
|
||||||
Timber.d("## VOIP Audio focus request failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
|
|
||||||
// required to be in this mode when playout and/or recording starts for
|
|
||||||
// best possible VoIP performance.
|
|
||||||
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
||||||
|
|
||||||
// Always disable microphone mute during a WebRTC call.
|
|
||||||
setMicrophoneMute(false)
|
|
||||||
|
|
||||||
adjustCurrentSoundDevice(mxCall)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun adjustCurrentSoundDevice(mxCall: MxCall) {
|
|
||||||
val audioManager = audioManager ?: return
|
|
||||||
executor.execute {
|
|
||||||
if (mxCall.state == CallState.LocalRinging && !isHeadsetOn()) {
|
|
||||||
// Always use speaker if incoming call is in ringing state and a headset is not connected
|
|
||||||
Timber.v("##VOIP: AudioManager default to SPEAKER (it is ringing)")
|
|
||||||
setCurrentSoundDevice(SoundDevice.SPEAKER)
|
|
||||||
} else if (mxCall.isVideoCall && !isHeadsetOn()) {
|
|
||||||
// If there are no headset, start video output in speaker
|
|
||||||
// (you can't watch the video and have the phone close to your ear)
|
|
||||||
Timber.v("##VOIP: AudioManager default to speaker ")
|
|
||||||
setCurrentSoundDevice(SoundDevice.SPEAKER)
|
|
||||||
} else {
|
|
||||||
// if a wired headset is plugged, sound will be directed to it
|
|
||||||
// (can't really force earpiece when headset is plugged)
|
|
||||||
if (isBluetoothHeadsetConnected(audioManager)) {
|
|
||||||
Timber.v("##VOIP: AudioManager default to WIRELESS_HEADSET ")
|
|
||||||
setCurrentSoundDevice(SoundDevice.WIRELESS_HEADSET)
|
|
||||||
// try now in case already connected?
|
|
||||||
audioManager.isBluetoothScoOn = true
|
|
||||||
} else {
|
|
||||||
Timber.v("##VOIP: AudioManager default to PHONE/HEADSET ")
|
|
||||||
setCurrentSoundDevice(if (isWiredHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onCallConnected(mxCall: MxCall) {
|
|
||||||
Timber.v("##VOIP: AudioManager call answered, adjusting current sound device")
|
|
||||||
setupAudioManager(mxCall)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAvailableSoundDevices(): List<SoundDevice> {
|
|
||||||
return ArrayList<SoundDevice>().apply {
|
|
||||||
if (isBluetoothHeadsetOn()) add(SoundDevice.WIRELESS_HEADSET)
|
|
||||||
add(if (isWiredHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE)
|
|
||||||
add(SoundDevice.SPEAKER)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
Timber.v("## VOIP: AudioManager stopCall")
|
|
||||||
executor.execute {
|
|
||||||
// Restore previously stored audio states.
|
|
||||||
setSpeakerphoneOn(savedIsSpeakerPhoneOn)
|
|
||||||
setMicrophoneMute(savedIsMicrophoneMute)
|
|
||||||
audioManager?.mode = savedAudioMode
|
|
||||||
|
|
||||||
connectedBlueToothHeadset?.let {
|
|
||||||
if (audioManager != null && isBluetoothHeadsetConnected(audioManager!!)) {
|
|
||||||
audioManager?.stopBluetoothSco()
|
|
||||||
audioManager?.isBluetoothScoOn = false
|
|
||||||
audioManager?.isSpeakerphoneOn = false
|
|
||||||
}
|
|
||||||
bluetoothAdapter?.closeProfileProxy(BluetoothProfile.HEADSET, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
audioManager?.mode = AudioManager.MODE_NORMAL
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
audioManager?.abandonAudioFocus(audioFocusChangeListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentSoundDevice(): SoundDevice {
|
|
||||||
val audioManager = audioManager ?: return SoundDevice.PHONE
|
|
||||||
if (audioManager.isSpeakerphoneOn) {
|
|
||||||
return SoundDevice.SPEAKER
|
|
||||||
} else {
|
|
||||||
if (isBluetoothHeadsetConnected(audioManager)) return SoundDevice.WIRELESS_HEADSET
|
|
||||||
return if (isHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isBluetoothHeadsetConnected(audioManager: AudioManager) =
|
|
||||||
isBluetoothHeadsetOn()
|
|
||||||
&& !connectedBlueToothHeadset?.connectedDevices.isNullOrEmpty()
|
|
||||||
&& (wantsBluetoothConnection || audioManager.isBluetoothScoOn)
|
|
||||||
|
|
||||||
fun setCurrentSoundDevice(device: SoundDevice) {
|
|
||||||
executor.execute {
|
|
||||||
Timber.v("## VOIP setCurrentSoundDevice $device")
|
|
||||||
when (device) {
|
|
||||||
SoundDevice.HEADSET,
|
|
||||||
SoundDevice.PHONE -> {
|
|
||||||
wantsBluetoothConnection = false
|
|
||||||
if (isBluetoothHeadsetOn()) {
|
|
||||||
audioManager?.stopBluetoothSco()
|
|
||||||
audioManager?.isBluetoothScoOn = false
|
|
||||||
}
|
|
||||||
setSpeakerphoneOn(false)
|
|
||||||
}
|
|
||||||
SoundDevice.SPEAKER -> {
|
|
||||||
setSpeakerphoneOn(true)
|
|
||||||
wantsBluetoothConnection = false
|
|
||||||
audioManager?.stopBluetoothSco()
|
|
||||||
audioManager?.isBluetoothScoOn = false
|
|
||||||
}
|
|
||||||
SoundDevice.WIRELESS_HEADSET -> {
|
|
||||||
setSpeakerphoneOn(false)
|
|
||||||
// I cannot directly do it, i have to start then wait that it's connected
|
|
||||||
// to route to bt
|
|
||||||
audioManager?.startBluetoothSco()
|
|
||||||
wantsBluetoothConnection = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configChange?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bluetoothStateChange(plugged: Boolean) {
|
|
||||||
executor.execute {
|
|
||||||
if (plugged && wantsBluetoothConnection) {
|
|
||||||
audioManager?.isBluetoothScoOn = true
|
|
||||||
} else if (!plugged && !wantsBluetoothConnection) {
|
|
||||||
audioManager?.stopBluetoothSco()
|
|
||||||
}
|
|
||||||
|
|
||||||
configChange?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun wiredStateChange(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
|
|
||||||
executor.execute {
|
|
||||||
// if it's plugged and speaker is on we should route to headset
|
|
||||||
if (event.plugged && getCurrentSoundDevice() == SoundDevice.SPEAKER) {
|
|
||||||
setCurrentSoundDevice(CallAudioManager.SoundDevice.HEADSET)
|
|
||||||
} else if (!event.plugged) {
|
|
||||||
// if it's unplugged ? always route to speaker?
|
|
||||||
// this is questionable?
|
|
||||||
if (!wantsBluetoothConnection) {
|
|
||||||
setCurrentSoundDevice(SoundDevice.SPEAKER)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
configChange?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isHeadsetOn(): Boolean {
|
|
||||||
return isWiredHeadsetOn() || (audioManager?.let { isBluetoothHeadsetConnected(it) } ?: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isWiredHeadsetOn(): Boolean {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
return audioManager?.isWiredHeadsetOn ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isBluetoothHeadsetOn(): Boolean {
|
|
||||||
Timber.v("## VOIP: AudioManager isBluetoothHeadsetOn")
|
|
||||||
try {
|
|
||||||
if (connectedBlueToothHeadset == null) return false.also {
|
|
||||||
Timber.v("## VOIP: AudioManager no connected bluetooth headset")
|
|
||||||
}
|
|
||||||
if (audioManager?.isBluetoothScoAvailableOffCall == false) return false.also {
|
|
||||||
Timber.v("## VOIP: AudioManager isBluetoothScoAvailableOffCall false")
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.e("## VOIP: AudioManager isBluetoothHeadsetOn failure ${failure.localizedMessage}")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the speaker phone mode. */
|
|
||||||
private fun setSpeakerphoneOn(on: Boolean) {
|
|
||||||
Timber.v("## VOIP: AudioManager setSpeakerphoneOn $on")
|
|
||||||
val wasOn = audioManager?.isSpeakerphoneOn ?: false
|
|
||||||
if (wasOn == on) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
audioManager?.isSpeakerphoneOn = on
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the microphone mute state. */
|
|
||||||
private fun setMicrophoneMute(on: Boolean) {
|
|
||||||
Timber.v("## VOIP: AudioManager setMicrophoneMute $on")
|
|
||||||
val wasMuted = audioManager?.isMicrophoneMute ?: false
|
|
||||||
if (wasMuted == on) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
audioManager?.isMicrophoneMute = on
|
|
||||||
}
|
|
||||||
|
|
||||||
/** true if the device has a telephony radio with data
|
|
||||||
* communication support. */
|
|
||||||
private fun isThisPhone(): Boolean {
|
|
||||||
return applicationContext.packageManager.hasSystemFeature(
|
|
||||||
PackageManager.FEATURE_TELEPHONY)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -27,6 +27,7 @@ import com.airbnb.mvrx.activityViewModel
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||||
import im.vector.app.databinding.BottomSheetCallControlsBinding
|
import im.vector.app.databinding.BottomSheetCallControlsBinding
|
||||||
|
import im.vector.app.features.call.audio.CallAudioManager
|
||||||
|
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
|
|
||||||
|
@ -44,20 +45,34 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
||||||
renderState(it)
|
renderState(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
views.callControlsSoundDevice.views.itemVerificationClickableZone.debouncedClicks {
|
views.callControlsSoundDevice.views.bottomSheetActionClickableZone.debouncedClicks {
|
||||||
callViewModel.handle(VectorCallViewActions.SwitchSoundDevice)
|
callViewModel.handle(VectorCallViewActions.SwitchSoundDevice)
|
||||||
}
|
}
|
||||||
|
|
||||||
views.callControlsSwitchCamera.views.itemVerificationClickableZone.debouncedClicks {
|
views.callControlsSwitchCamera.views.bottomSheetActionClickableZone.debouncedClicks {
|
||||||
callViewModel.handle(VectorCallViewActions.ToggleCamera)
|
callViewModel.handle(VectorCallViewActions.ToggleCamera)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
views.callControlsToggleSDHD.views.itemVerificationClickableZone.debouncedClicks {
|
views.callControlsToggleSDHD.views.bottomSheetActionClickableZone.debouncedClicks {
|
||||||
callViewModel.handle(VectorCallViewActions.ToggleHDSD)
|
callViewModel.handle(VectorCallViewActions.ToggleHDSD)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
views.callControlsToggleHoldResume.views.bottomSheetActionClickableZone.debouncedClicks {
|
||||||
|
callViewModel.handle(VectorCallViewActions.ToggleHoldResume)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
views.callControlsOpenDialPad.views.bottomSheetActionClickableZone.debouncedClicks {
|
||||||
|
callViewModel.handle(VectorCallViewActions.OpenDialPad)
|
||||||
|
}
|
||||||
|
|
||||||
|
views.callControlsTransfer.views.bottomSheetActionClickableZone.debouncedClicks {
|
||||||
|
callViewModel.handle(VectorCallViewActions.InitiateCallTransfer)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
callViewModel.observeViewEvents {
|
callViewModel.observeViewEvents {
|
||||||
when (it) {
|
when (it) {
|
||||||
is VectorCallViewEvents.ShowSoundDeviceChooser -> {
|
is VectorCallViewEvents.ShowSoundDeviceChooser -> {
|
||||||
|
@ -69,22 +84,22 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showSoundDeviceChooser(available: List<CallAudioManager.SoundDevice>, current: CallAudioManager.SoundDevice) {
|
private fun showSoundDeviceChooser(available: Set<CallAudioManager.Device>, current: CallAudioManager.Device) {
|
||||||
val soundDevices = available.map {
|
val soundDevices = available.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
CallAudioManager.SoundDevice.WIRELESS_HEADSET -> span {
|
CallAudioManager.Device.WIRELESS_HEADSET -> span {
|
||||||
text = getString(R.string.sound_device_wireless_headset)
|
text = getString(R.string.sound_device_wireless_headset)
|
||||||
textStyle = if (current == it) "bold" else "normal"
|
textStyle = if (current == it) "bold" else "normal"
|
||||||
}
|
}
|
||||||
CallAudioManager.SoundDevice.PHONE -> span {
|
CallAudioManager.Device.PHONE -> span {
|
||||||
text = getString(R.string.sound_device_phone)
|
text = getString(R.string.sound_device_phone)
|
||||||
textStyle = if (current == it) "bold" else "normal"
|
textStyle = if (current == it) "bold" else "normal"
|
||||||
}
|
}
|
||||||
CallAudioManager.SoundDevice.SPEAKER -> span {
|
CallAudioManager.Device.SPEAKER -> span {
|
||||||
text = getString(R.string.sound_device_speaker)
|
text = getString(R.string.sound_device_speaker)
|
||||||
textStyle = if (current == it) "bold" else "normal"
|
textStyle = if (current == it) "bold" else "normal"
|
||||||
}
|
}
|
||||||
CallAudioManager.SoundDevice.HEADSET -> span {
|
CallAudioManager.Device.HEADSET -> span {
|
||||||
text = getString(R.string.sound_device_headset)
|
text = getString(R.string.sound_device_headset)
|
||||||
textStyle = if (current == it) "bold" else "normal"
|
textStyle = if (current == it) "bold" else "normal"
|
||||||
}
|
}
|
||||||
|
@ -95,17 +110,17 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
||||||
d.cancel()
|
d.cancel()
|
||||||
when (soundDevices[n].toString()) {
|
when (soundDevices[n].toString()) {
|
||||||
// TODO Make an adapter and handle multiple Bluetooth headsets. Also do not use translations.
|
// TODO Make an adapter and handle multiple Bluetooth headsets. Also do not use translations.
|
||||||
getString(R.string.sound_device_phone) -> {
|
getString(R.string.sound_device_phone) -> {
|
||||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.PHONE))
|
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.PHONE))
|
||||||
}
|
}
|
||||||
getString(R.string.sound_device_speaker) -> {
|
getString(R.string.sound_device_speaker) -> {
|
||||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.SPEAKER))
|
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.SPEAKER))
|
||||||
}
|
}
|
||||||
getString(R.string.sound_device_headset) -> {
|
getString(R.string.sound_device_headset) -> {
|
||||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.HEADSET))
|
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.HEADSET))
|
||||||
}
|
}
|
||||||
getString(R.string.sound_device_wireless_headset) -> {
|
getString(R.string.sound_device_wireless_headset) -> {
|
||||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.WIRELESS_HEADSET))
|
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.WIRELESS_HEADSET))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,11 +130,11 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
||||||
|
|
||||||
private fun renderState(state: VectorCallViewState) {
|
private fun renderState(state: VectorCallViewState) {
|
||||||
views.callControlsSoundDevice.title = getString(R.string.call_select_sound_device)
|
views.callControlsSoundDevice.title = getString(R.string.call_select_sound_device)
|
||||||
views.callControlsSoundDevice.subTitle = when (state.soundDevice) {
|
views.callControlsSoundDevice.subTitle = when (state.device) {
|
||||||
CallAudioManager.SoundDevice.PHONE -> getString(R.string.sound_device_phone)
|
CallAudioManager.Device.PHONE -> getString(R.string.sound_device_phone)
|
||||||
CallAudioManager.SoundDevice.SPEAKER -> getString(R.string.sound_device_speaker)
|
CallAudioManager.Device.SPEAKER -> getString(R.string.sound_device_speaker)
|
||||||
CallAudioManager.SoundDevice.HEADSET -> getString(R.string.sound_device_headset)
|
CallAudioManager.Device.HEADSET -> getString(R.string.sound_device_headset)
|
||||||
CallAudioManager.SoundDevice.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset)
|
CallAudioManager.Device.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset)
|
||||||
}
|
}
|
||||||
|
|
||||||
views.callControlsSwitchCamera.isVisible = state.isVideoCall && state.canSwitchCamera
|
views.callControlsSwitchCamera.isVisible = state.isVideoCall && state.canSwitchCamera
|
||||||
|
@ -139,5 +154,15 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
||||||
} else {
|
} else {
|
||||||
views.callControlsToggleSDHD.isVisible = false
|
views.callControlsToggleSDHD.isVisible = false
|
||||||
}
|
}
|
||||||
|
if (state.isRemoteOnHold) {
|
||||||
|
views.callControlsToggleHoldResume.title = getString(R.string.call_resume_action)
|
||||||
|
views.callControlsToggleHoldResume.subTitle = null
|
||||||
|
views.callControlsToggleHoldResume.leftIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_call_resume_action)
|
||||||
|
} else {
|
||||||
|
views.callControlsToggleHoldResume.title = getString(R.string.call_hold_action)
|
||||||
|
views.callControlsToggleHoldResume.subTitle = null
|
||||||
|
views.callControlsToggleHoldResume.leftIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_call_hold_action)
|
||||||
|
}
|
||||||
|
views.callControlsTransfer.isVisible = state.canOpponentBeTransferred
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,8 @@ import android.widget.FrameLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.databinding.ViewCallControlsBinding
|
import im.vector.app.databinding.ViewCallControlsBinding
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
import org.matrix.android.sdk.api.session.call.CallState
|
||||||
import org.webrtc.PeerConnection
|
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
||||||
|
|
||||||
class CallControlsView @JvmOverloads constructor(
|
class CallControlsView @JvmOverloads constructor(
|
||||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||||
|
@ -36,16 +35,15 @@ class CallControlsView @JvmOverloads constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
inflate(context, R.layout.view_call_controls, this)
|
inflate(context, R.layout.view_call_controls, this)
|
||||||
// layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
||||||
views = ViewCallControlsBinding.bind(this)
|
views = ViewCallControlsBinding.bind(this)
|
||||||
|
|
||||||
views.ringingControlAccept.setOnClickListener { acceptIncomingCall() }
|
views.ringingControlAccept.setOnClickListener { acceptIncomingCall() }
|
||||||
views.ringingControlDecline.setOnClickListener { declineIncomingCall() }
|
views.ringingControlDecline.setOnClickListener { declineIncomingCall() }
|
||||||
views.ivEndCall.setOnClickListener { endOngoingCall() }
|
views.endCallIcon.setOnClickListener { endOngoingCall() }
|
||||||
views.muteIcon.setOnClickListener { toggleMute() }
|
views.muteIcon.setOnClickListener { toggleMute() }
|
||||||
views.videoToggleIcon.setOnClickListener { toggleVideo() }
|
views.videoToggleIcon.setOnClickListener { toggleVideo() }
|
||||||
views.ivLeftMiniControl.setOnClickListener { returnToChat() }
|
views.openChatIcon.setOnClickListener { returnToChat() }
|
||||||
views.ivMore.setOnClickListener { moreControlOption() }
|
views.moreIcon.setOnClickListener { moreControlOption() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun acceptIncomingCall() {
|
private fun acceptIncomingCall() {
|
||||||
|
@ -109,7 +107,7 @@ class CallControlsView @JvmOverloads constructor(
|
||||||
views.connectedControls.isVisible = false
|
views.connectedControls.isVisible = false
|
||||||
}
|
}
|
||||||
is CallState.Connected -> {
|
is CallState.Connected -> {
|
||||||
if (callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) {
|
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
|
||||||
views.ringingControls.isVisible = false
|
views.ringingControls.isVisible = false
|
||||||
views.connectedControls.isVisible = true
|
views.connectedControls.isVisible = true
|
||||||
views.videoToggleIcon.isVisible = state.isVideoCall
|
views.videoToggleIcon.isVisible = state.isVideoCall
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.call
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||||
|
import im.vector.app.databinding.BottomSheetCallDialerChoiceBinding
|
||||||
|
|
||||||
|
class DialerChoiceBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCallDialerChoiceBinding>() {
|
||||||
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetCallDialerChoiceBinding {
|
||||||
|
return BottomSheetCallDialerChoiceBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var onDialPadClicked: (() -> Unit)? = null
|
||||||
|
var onVoiceCallClicked: (() -> Unit)? = null
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
views.dialerChoiceDialPad.views.bottomSheetActionClickableZone.debouncedClicks {
|
||||||
|
onDialPadClicked?.invoke()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
views.dialerChoiceVoiceCall.views.bottomSheetActionClickableZone.debouncedClicks {
|
||||||
|
onVoiceCallClicked?.invoke()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,57 +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.app.features.call
|
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import org.matrix.android.sdk.api.session.call.MxCall
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class SharedActiveCallViewModel @Inject constructor(
|
|
||||||
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
val activeCall: MutableLiveData<MxCall?> = MutableLiveData()
|
|
||||||
|
|
||||||
val callStateListener = object : MxCall.StateListener {
|
|
||||||
|
|
||||||
override fun onStateUpdate(call: MxCall) {
|
|
||||||
if (activeCall.value?.callId == call.callId) {
|
|
||||||
activeCall.postValue(call)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val listener = object : WebRtcPeerConnectionManager.CurrentCallListener {
|
|
||||||
override fun onCurrentCallChange(call: MxCall?) {
|
|
||||||
activeCall.value?.removeListener(callStateListener)
|
|
||||||
activeCall.postValue(call)
|
|
||||||
call?.addListener(callStateListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
activeCall.postValue(webRtcPeerConnectionManager.currentCall?.mxCall)
|
|
||||||
webRtcPeerConnectionManager.addCurrentCallListener(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
activeCall.value?.removeListener(callStateListener)
|
|
||||||
webRtcPeerConnectionManager.removeCurrentCallListener(listener)
|
|
||||||
super.onCleared()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* 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.app.features.call
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||||
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
|
import org.matrix.android.sdk.api.session.call.MxCall
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class SharedKnownCallsViewModel @Inject constructor(
|
||||||
|
private val callManager: WebRtcCallManager
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val liveKnownCalls: MutableLiveData<List<WebRtcCall>> = MutableLiveData()
|
||||||
|
|
||||||
|
val callListener = object : WebRtcCall.Listener {
|
||||||
|
|
||||||
|
override fun onStateUpdate(call: MxCall) {
|
||||||
|
// post it-self
|
||||||
|
liveKnownCalls.postValue(liveKnownCalls.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHoldUnhold() {
|
||||||
|
super.onHoldUnhold()
|
||||||
|
// post it-self
|
||||||
|
liveKnownCalls.postValue(liveKnownCalls.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val currentCallListener = object : WebRtcCallManager.CurrentCallListener {
|
||||||
|
override fun onCurrentCallChange(call: WebRtcCall?) {
|
||||||
|
val knownCalls = callManager.getCalls()
|
||||||
|
liveKnownCalls.postValue(knownCalls)
|
||||||
|
knownCalls.forEach {
|
||||||
|
it.removeListener(callListener)
|
||||||
|
it.addListener(callListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val knownCalls = callManager.getCalls()
|
||||||
|
liveKnownCalls.postValue(knownCalls)
|
||||||
|
callManager.addCurrentCallListener(currentCallListener)
|
||||||
|
knownCalls.forEach {
|
||||||
|
it.addListener(callListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
callManager.getCalls().forEach {
|
||||||
|
it.removeListener(callListener)
|
||||||
|
}
|
||||||
|
callManager.removeCurrentCallListener(currentCallListener)
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,54 +20,52 @@ import android.app.KeyguardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
|
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.Window
|
|
||||||
import android.view.WindowInsets
|
|
||||||
import android.view.WindowInsetsController
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import com.airbnb.mvrx.Fail
|
import com.airbnb.mvrx.Fail
|
||||||
import com.airbnb.mvrx.MvRx
|
import com.airbnb.mvrx.MvRx
|
||||||
import com.airbnb.mvrx.viewModel
|
import com.airbnb.mvrx.viewModel
|
||||||
import com.jakewharton.rxbinding3.view.clicks
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.ScreenComponent
|
import im.vector.app.core.di.ScreenComponent
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
import im.vector.app.core.services.CallService
|
|
||||||
import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
|
import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
|
||||||
import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
|
import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
|
||||||
import im.vector.app.core.utils.allGranted
|
import im.vector.app.core.utils.allGranted
|
||||||
import im.vector.app.core.utils.checkPermissions
|
import im.vector.app.core.utils.checkPermissions
|
||||||
import im.vector.app.databinding.ActivityCallBinding
|
import im.vector.app.databinding.ActivityCallBinding
|
||||||
|
import im.vector.app.features.call.dialpad.CallDialPadBottomSheet
|
||||||
|
import im.vector.app.features.call.dialpad.DialPadFragment
|
||||||
|
import im.vector.app.features.call.utils.EglUtils
|
||||||
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailActivity
|
import im.vector.app.features.home.room.detail.RoomDetailActivity
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailArgs
|
import im.vector.app.features.home.room.detail.RoomDetailArgs
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
import org.matrix.android.sdk.api.session.call.CallState
|
||||||
import org.matrix.android.sdk.api.session.call.EglUtils
|
|
||||||
import org.matrix.android.sdk.api.session.call.MxCallDetail
|
import org.matrix.android.sdk.api.session.call.MxCallDetail
|
||||||
|
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
||||||
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||||
import org.webrtc.EglBase
|
import org.webrtc.EglBase
|
||||||
import org.webrtc.PeerConnection
|
|
||||||
import org.webrtc.RendererCommon
|
import org.webrtc.RendererCommon
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class CallArgs(
|
data class CallArgs(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val callId: String?,
|
val callId: String,
|
||||||
val participantUserId: String,
|
val participantUserId: String,
|
||||||
val isIncomingCall: Boolean,
|
val isIncomingCall: Boolean,
|
||||||
val isVideoCall: Boolean
|
val isVideoCall: Boolean
|
||||||
|
@ -87,101 +85,36 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
private val callViewModel: VectorCallViewModel by viewModel()
|
private val callViewModel: VectorCallViewModel by viewModel()
|
||||||
private lateinit var callArgs: CallArgs
|
private lateinit var callArgs: CallArgs
|
||||||
|
|
||||||
@Inject lateinit var peerConnectionManager: WebRtcPeerConnectionManager
|
@Inject lateinit var callManager: WebRtcCallManager
|
||||||
|
|
||||||
@Inject lateinit var viewModelFactory: VectorCallViewModel.Factory
|
@Inject lateinit var viewModelFactory: VectorCallViewModel.Factory
|
||||||
|
|
||||||
private var rootEglBase: EglBase? = null
|
private val dialPadCallback = object : DialPadFragment.Callback {
|
||||||
|
override fun onDigitAppended(digit: String) {
|
||||||
|
callViewModel.handle(VectorCallViewActions.SendDtmfDigit(digit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var systemUiVisibility = false
|
private var rootEglBase: EglBase? = null
|
||||||
|
|
||||||
var surfaceRenderersAreInitialized = false
|
var surfaceRenderersAreInitialized = false
|
||||||
|
|
||||||
override fun doBeforeSetContentView() {
|
override fun doBeforeSetContentView() {
|
||||||
// Set window styles for fullscreen-window size. Needs to be done before adding content.
|
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
|
||||||
|
|
||||||
hideSystemUI()
|
|
||||||
setContentView(R.layout.activity_call)
|
setContentView(R.layout.activity_call)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun hideSystemUI() {
|
|
||||||
systemUiVisibility = false
|
|
||||||
// Enables regular immersive mode.
|
|
||||||
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
|
|
||||||
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
||||||
window.setDecorFitsSystemWindows(false)
|
|
||||||
// New API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
||||||
window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
|
|
||||||
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
|
||||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
|
||||||
// New API instead of FLAG_TRANSLUCENT_STATUS
|
|
||||||
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
|
||||||
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
|
|
||||||
window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
|
||||||
} else {
|
|
||||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
|
||||||
// Set the content to appear under the system bars so that the
|
|
||||||
// content doesn't resize when the system bars hide and show.
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
||||||
// Hide the nav bar and status bar
|
|
||||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
||||||
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shows the system bars by removing all the flags
|
|
||||||
// except for the ones that make the content appear under the system bars.
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun showSystemUI() {
|
|
||||||
systemUiVisibility = true
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
||||||
window.setDecorFitsSystemWindows(false)
|
|
||||||
} else {
|
|
||||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleUiSystemVisibility() {
|
|
||||||
if (systemUiVisibility) {
|
|
||||||
hideSystemUI()
|
|
||||||
} else {
|
|
||||||
showSystemUI()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
|
||||||
super.onWindowFocusChanged(hasFocus)
|
|
||||||
// Rehide when bottom sheet is dismissed
|
|
||||||
if (hasFocus) {
|
|
||||||
hideSystemUI()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
@Suppress("DEPRECATION")
|
||||||
|
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||||
// This will need to be refined
|
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(views.constraintLayout) { v, insets ->
|
|
||||||
v.updatePadding(bottom = if (systemUiVisibility) insets.systemWindowInsetBottom else 0)
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
window.statusBarColor = Color.TRANSPARENT
|
||||||
|
window.navigationBarColor = Color.BLACK
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (intent.hasExtra(MvRx.KEY_ARG)) {
|
if (intent.hasExtra(MvRx.KEY_ARG)) {
|
||||||
callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!!
|
callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!!
|
||||||
} else {
|
} else {
|
||||||
Timber.e("## VOIP missing callArgs for VectorCall Activity")
|
Timber.e("## VOIP missing callArgs for VectorCall Activity")
|
||||||
CallService.onNoActiveCall(this)
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,13 +122,9 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
if (intent.getStringExtra(EXTRA_MODE) == INCOMING_RINGING) {
|
if (intent.getStringExtra(EXTRA_MODE) == INCOMING_RINGING) {
|
||||||
turnScreenOnAndKeyguardOff()
|
turnScreenOnAndKeyguardOff()
|
||||||
}
|
}
|
||||||
|
if (savedInstanceState != null) {
|
||||||
views.constraintLayout.clicks()
|
(supportFragmentManager.findFragmentByTag(FRAGMENT_DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.callback = dialPadCallback
|
||||||
.throttleFirst(300, TimeUnit.MILLISECONDS)
|
}
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { toggleUiSystemVisibility() }
|
|
||||||
.disposeOnDestroy()
|
|
||||||
|
|
||||||
configureCallViews()
|
configureCallViews()
|
||||||
|
|
||||||
callViewModel.subscribe(this) {
|
callViewModel.subscribe(this) {
|
||||||
|
@ -222,7 +151,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
peerConnectionManager.detachRenderers(listOf(views.pipRenderer, views.fullscreenRenderer))
|
callManager.getCallById(callArgs.callId)?.detachRenderers(listOf(views.pipRenderer, views.fullscreenRenderer))
|
||||||
if (surfaceRenderersAreInitialized) {
|
if (surfaceRenderersAreInitialized) {
|
||||||
views.pipRenderer.release()
|
views.pipRenderer.release()
|
||||||
views.fullscreenRenderer.release()
|
views.fullscreenRenderer.release()
|
||||||
|
@ -234,8 +163,6 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
private fun renderState(state: VectorCallViewState) {
|
private fun renderState(state: VectorCallViewState) {
|
||||||
Timber.v("## VOIP renderState call $state")
|
Timber.v("## VOIP renderState call $state")
|
||||||
if (state.callState is Fail) {
|
if (state.callState is Fail) {
|
||||||
// be sure to clear notification
|
|
||||||
CallService.onNoActiveCall(this)
|
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -243,9 +170,13 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
views.callControlsView.updateForState(state)
|
views.callControlsView.updateForState(state)
|
||||||
val callState = state.callState.invoke()
|
val callState = state.callState.invoke()
|
||||||
views.callConnectingProgress.isVisible = false
|
views.callConnectingProgress.isVisible = false
|
||||||
|
views.callActionText.setOnClickListener(null)
|
||||||
|
views.callActionText.isVisible = false
|
||||||
|
views.smallIsHeldIcon.isVisible = false
|
||||||
when (callState) {
|
when (callState) {
|
||||||
is CallState.Idle,
|
is CallState.Idle,
|
||||||
is CallState.Dialing -> {
|
is CallState.CreateOffer,
|
||||||
|
is CallState.Dialing -> {
|
||||||
views.callVideoGroup.isInvisible = true
|
views.callVideoGroup.isInvisible = true
|
||||||
views.callInfoGroup.isVisible = true
|
views.callInfoGroup.isVisible = true
|
||||||
views.callStatusText.setText(R.string.call_ring)
|
views.callStatusText.setText(R.string.call_ring)
|
||||||
|
@ -259,24 +190,42 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
configureCallInfo(state)
|
configureCallInfo(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
is CallState.Answering -> {
|
is CallState.Answering -> {
|
||||||
views.callVideoGroup.isInvisible = true
|
views.callVideoGroup.isInvisible = true
|
||||||
views.callInfoGroup.isVisible = true
|
views.callInfoGroup.isVisible = true
|
||||||
views.callStatusText.setText(R.string.call_connecting)
|
views.callStatusText.setText(R.string.call_connecting)
|
||||||
views.callConnectingProgress.isVisible = true
|
views.callConnectingProgress.isVisible = true
|
||||||
configureCallInfo(state)
|
configureCallInfo(state)
|
||||||
}
|
}
|
||||||
is CallState.Connected -> {
|
is CallState.Connected -> {
|
||||||
if (callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) {
|
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
|
||||||
if (callArgs.isVideoCall) {
|
if (state.isLocalOnHold || state.isRemoteOnHold) {
|
||||||
views.callVideoGroup.isVisible = true
|
views.smallIsHeldIcon.isVisible = true
|
||||||
views.callInfoGroup.isVisible = false
|
|
||||||
views.pipRenderer.isVisible = !state.isVideoCaptureInError
|
|
||||||
} else {
|
|
||||||
views.callVideoGroup.isInvisible = true
|
views.callVideoGroup.isInvisible = true
|
||||||
views.callInfoGroup.isVisible = true
|
views.callInfoGroup.isVisible = true
|
||||||
|
configureCallInfo(state, blurAvatar = true)
|
||||||
|
if (state.isRemoteOnHold) {
|
||||||
|
views.callActionText.setText(R.string.call_resume_action)
|
||||||
|
views.callActionText.isVisible = true
|
||||||
|
views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.ToggleHoldResume) }
|
||||||
|
views.callStatusText.setText(R.string.call_held_by_you)
|
||||||
|
} else {
|
||||||
|
views.callActionText.isInvisible = true
|
||||||
|
state.callInfo.otherUserItem?.let {
|
||||||
|
views.callStatusText.text = getString(R.string.call_held_by_user, it.getBestName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
views.callStatusText.text = state.formattedDuration
|
||||||
configureCallInfo(state)
|
configureCallInfo(state)
|
||||||
views.callStatusText.text = null
|
if (callArgs.isVideoCall) {
|
||||||
|
views.callVideoGroup.isVisible = true
|
||||||
|
views.callInfoGroup.isVisible = false
|
||||||
|
views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null
|
||||||
|
} else {
|
||||||
|
views.callVideoGroup.isInvisible = true
|
||||||
|
views.callInfoGroup.isVisible = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// This state is not final, if you change network, new candidates will be sent
|
// This state is not final, if you change network, new candidates will be sent
|
||||||
|
@ -286,27 +235,52 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
views.callStatusText.setText(R.string.call_connecting)
|
views.callStatusText.setText(R.string.call_connecting)
|
||||||
views.callConnectingProgress.isVisible = true
|
views.callConnectingProgress.isVisible = true
|
||||||
}
|
}
|
||||||
// ensure all attached?
|
|
||||||
peerConnectionManager.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer, null)
|
|
||||||
}
|
}
|
||||||
is CallState.Terminated -> {
|
is CallState.Terminated -> {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
null -> {
|
null -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configureCallInfo(state: VectorCallViewState) {
|
private fun configureCallInfo(state: VectorCallViewState, blurAvatar: Boolean = false) {
|
||||||
state.otherUserMatrixItem.invoke()?.let {
|
state.callInfo.otherUserItem?.let {
|
||||||
avatarRenderer.render(it, views.otherMemberAvatar)
|
val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen)
|
||||||
|
avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter)
|
||||||
views.participantNameText.text = it.getBestName()
|
views.participantNameText.text = it.getBestName()
|
||||||
views.callTypeText.setText(if (state.isVideoCall) R.string.action_video_call else R.string.action_voice_call)
|
if (blurAvatar) {
|
||||||
|
avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter)
|
||||||
|
} else {
|
||||||
|
avatarRenderer.render(it, views.otherMemberAvatar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.otherKnownCallInfo?.otherUserItem == null) {
|
||||||
|
views.otherKnownCallLayout.isVisible = false
|
||||||
|
} else {
|
||||||
|
val otherCall = callManager.getCallById(state.otherKnownCallInfo.callId)
|
||||||
|
val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen)
|
||||||
|
avatarRenderer.renderBlur(
|
||||||
|
matrixItem = state.otherKnownCallInfo.otherUserItem,
|
||||||
|
imageView = views.otherKnownCallAvatarView,
|
||||||
|
sampling = 20,
|
||||||
|
rounded = false,
|
||||||
|
colorFilter = colorFilter
|
||||||
|
)
|
||||||
|
views.otherKnownCallLayout.isVisible = true
|
||||||
|
views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.remoteOnHold }.orFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configureCallViews() {
|
private fun configureCallViews() {
|
||||||
views.callControlsView.interactionListener = this
|
views.callControlsView.interactionListener = this
|
||||||
|
views.otherKnownCallAvatarView.setOnClickListener {
|
||||||
|
withState(callViewModel) {
|
||||||
|
val otherCall = callManager.getCallById(it.otherKnownCallInfo?.callId ?: "") ?: return@withState
|
||||||
|
startActivity(newIntent(this, otherCall.mxCall, null))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
@ -331,17 +305,14 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
|
|
||||||
// Init Full Screen renderer
|
// Init Full Screen renderer
|
||||||
views.fullscreenRenderer.init(rootEglBase!!.eglBaseContext, null)
|
views.fullscreenRenderer.init(rootEglBase!!.eglBaseContext, null)
|
||||||
views.fullscreenRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
views.fullscreenRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
|
||||||
|
|
||||||
views.pipRenderer.setZOrderMediaOverlay(true)
|
views.pipRenderer.setZOrderMediaOverlay(true)
|
||||||
views.pipRenderer.setEnableHardwareScaler(true /* enabled */)
|
views.pipRenderer.setEnableHardwareScaler(true /* enabled */)
|
||||||
views.fullscreenRenderer.setEnableHardwareScaler(true /* enabled */)
|
views.fullscreenRenderer.setEnableHardwareScaler(true /* enabled */)
|
||||||
|
|
||||||
peerConnectionManager.attachViewRenderers(
|
callManager.getCallById(callArgs.callId)?.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer,
|
||||||
views.pipRenderer,
|
intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() })
|
||||||
views.fullscreenRenderer,
|
|
||||||
intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() }
|
|
||||||
)
|
|
||||||
|
|
||||||
views.pipRenderer.setOnClickListener {
|
views.pipRenderer.setOnClickListener {
|
||||||
callViewModel.handle(VectorCallViewActions.ToggleCamera)
|
callViewModel.handle(VectorCallViewActions.ToggleCamera)
|
||||||
|
@ -352,14 +323,21 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
private fun handleViewEvents(event: VectorCallViewEvents?) {
|
private fun handleViewEvents(event: VectorCallViewEvents?) {
|
||||||
Timber.v("## VOIP handleViewEvents $event")
|
Timber.v("## VOIP handleViewEvents $event")
|
||||||
when (event) {
|
when (event) {
|
||||||
VectorCallViewEvents.DismissNoCall -> {
|
VectorCallViewEvents.DismissNoCall -> {
|
||||||
CallService.onNoActiveCall(this)
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
is VectorCallViewEvents.ConnectionTimeout -> {
|
is VectorCallViewEvents.ConnectionTimeout -> {
|
||||||
onErrorTimoutConnect(event.turn)
|
onErrorTimoutConnect(event.turn)
|
||||||
}
|
}
|
||||||
null -> {
|
is VectorCallViewEvents.ShowDialPad -> {
|
||||||
|
CallDialPadBottomSheet.newInstance(false).apply {
|
||||||
|
callback = dialPadCallback
|
||||||
|
}.show(supportFragmentManager, FRAGMENT_DIAL_PAD_TAG)
|
||||||
|
}
|
||||||
|
is VectorCallViewEvents.ShowCallTransferScreen -> {
|
||||||
|
navigator.openCallTransfer(this, callArgs.callId)
|
||||||
|
}
|
||||||
|
null -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -381,22 +359,23 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
|
|
||||||
private const val CAPTURE_PERMISSION_REQUEST_CODE = 1
|
private const val CAPTURE_PERMISSION_REQUEST_CODE = 1
|
||||||
private const val EXTRA_MODE = "EXTRA_MODE"
|
private const val EXTRA_MODE = "EXTRA_MODE"
|
||||||
|
private const val FRAGMENT_DIAL_PAD_TAG = "FRAGMENT_DIAL_PAD_TAG"
|
||||||
|
|
||||||
const val OUTGOING_CREATED = "OUTGOING_CREATED"
|
const val OUTGOING_CREATED = "OUTGOING_CREATED"
|
||||||
const val INCOMING_RINGING = "INCOMING_RINGING"
|
const val INCOMING_RINGING = "INCOMING_RINGING"
|
||||||
const val INCOMING_ACCEPT = "INCOMING_ACCEPT"
|
const val INCOMING_ACCEPT = "INCOMING_ACCEPT"
|
||||||
|
|
||||||
fun newIntent(context: Context, mxCall: MxCallDetail): Intent {
|
fun newIntent(context: Context, mxCall: MxCallDetail, mode: String?): Intent {
|
||||||
return Intent(context, VectorCallActivity::class.java).apply {
|
return Intent(context, VectorCallActivity::class.java).apply {
|
||||||
// what could be the best flags?
|
// what could be the best flags?
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
putExtra(MvRx.KEY_ARG, CallArgs(mxCall.roomId, mxCall.callId, mxCall.otherUserId, !mxCall.isOutgoing, mxCall.isVideoCall))
|
putExtra(MvRx.KEY_ARG, CallArgs(mxCall.roomId, mxCall.callId, mxCall.opponentUserId, !mxCall.isOutgoing, mxCall.isVideoCall))
|
||||||
putExtra(EXTRA_MODE, OUTGOING_CREATED)
|
putExtra(EXTRA_MODE, mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newIntent(context: Context,
|
fun newIntent(context: Context,
|
||||||
callId: String?,
|
callId: String,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
otherUserId: String,
|
otherUserId: String,
|
||||||
isIncomingCall: Boolean,
|
isIncomingCall: Boolean,
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.app.features.call
|
package im.vector.app.features.call
|
||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
import im.vector.app.features.call.audio.CallAudioManager
|
||||||
|
|
||||||
sealed class VectorCallViewActions : VectorViewModelAction {
|
sealed class VectorCallViewActions : VectorViewModelAction {
|
||||||
object EndCall : VectorCallViewActions()
|
object EndCall : VectorCallViewActions()
|
||||||
|
@ -24,9 +25,13 @@ sealed class VectorCallViewActions : VectorViewModelAction {
|
||||||
object DeclineCall : VectorCallViewActions()
|
object DeclineCall : VectorCallViewActions()
|
||||||
object ToggleMute : VectorCallViewActions()
|
object ToggleMute : VectorCallViewActions()
|
||||||
object ToggleVideo : VectorCallViewActions()
|
object ToggleVideo : VectorCallViewActions()
|
||||||
data class ChangeAudioDevice(val device: CallAudioManager.SoundDevice) : VectorCallViewActions()
|
object ToggleHoldResume: VectorCallViewActions()
|
||||||
|
data class ChangeAudioDevice(val device: CallAudioManager.Device) : VectorCallViewActions()
|
||||||
|
object OpenDialPad: VectorCallViewActions()
|
||||||
|
data class SendDtmfDigit(val digit: String) : VectorCallViewActions()
|
||||||
object SwitchSoundDevice : VectorCallViewActions()
|
object SwitchSoundDevice : VectorCallViewActions()
|
||||||
object HeadSetButtonPressed : VectorCallViewActions()
|
object HeadSetButtonPressed : VectorCallViewActions()
|
||||||
object ToggleCamera : VectorCallViewActions()
|
object ToggleCamera : VectorCallViewActions()
|
||||||
object ToggleHDSD : VectorCallViewActions()
|
object ToggleHDSD : VectorCallViewActions()
|
||||||
|
object InitiateCallTransfer : VectorCallViewActions()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.app.features.call
|
package im.vector.app.features.call
|
||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
import im.vector.app.features.call.audio.CallAudioManager
|
||||||
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||||
|
|
||||||
sealed class VectorCallViewEvents : VectorViewEvents {
|
sealed class VectorCallViewEvents : VectorViewEvents {
|
||||||
|
@ -24,9 +25,11 @@ sealed class VectorCallViewEvents : VectorViewEvents {
|
||||||
object DismissNoCall : VectorCallViewEvents()
|
object DismissNoCall : VectorCallViewEvents()
|
||||||
data class ConnectionTimeout(val turn: TurnServerResponse?) : VectorCallViewEvents()
|
data class ConnectionTimeout(val turn: TurnServerResponse?) : VectorCallViewEvents()
|
||||||
data class ShowSoundDeviceChooser(
|
data class ShowSoundDeviceChooser(
|
||||||
val available: List<CallAudioManager.SoundDevice>,
|
val available: Set<CallAudioManager.Device>,
|
||||||
val current: CallAudioManager.SoundDevice
|
val current: CallAudioManager.Device
|
||||||
) : VectorCallViewEvents()
|
) : VectorCallViewEvents()
|
||||||
|
object ShowDialPad: VectorCallViewEvents()
|
||||||
|
object ShowCallTransferScreen: VectorCallViewEvents()
|
||||||
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
|
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
|
||||||
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
|
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
|
||||||
// object CallAccepted : VectorCallViewEvents()
|
// object CallAccepted : VectorCallViewEvents()
|
||||||
|
|
|
@ -20,41 +20,77 @@ import com.airbnb.mvrx.Fail
|
||||||
import com.airbnb.mvrx.Loading
|
import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.Uninitialized
|
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import im.vector.app.features.call.audio.CallAudioManager
|
||||||
|
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||||
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
import org.matrix.android.sdk.api.session.call.CallState
|
||||||
import org.matrix.android.sdk.api.session.call.MxCall
|
import org.matrix.android.sdk.api.session.call.MxCall
|
||||||
|
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
||||||
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import org.webrtc.PeerConnection
|
|
||||||
import java.util.Timer
|
import java.util.Timer
|
||||||
import java.util.TimerTask
|
import java.util.TimerTask
|
||||||
|
|
||||||
class VectorCallViewModel @AssistedInject constructor(
|
class VectorCallViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: VectorCallViewState,
|
@Assisted initialState: VectorCallViewState,
|
||||||
@Assisted val args: CallArgs,
|
|
||||||
val session: Session,
|
val session: Session,
|
||||||
val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
|
val callManager: WebRtcCallManager,
|
||||||
val proximityManager: CallProximityManager
|
val proximityManager: CallProximityManager
|
||||||
) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) {
|
) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) {
|
||||||
|
|
||||||
private var call: MxCall? = null
|
private var call: WebRtcCall? = null
|
||||||
|
|
||||||
private var connectionTimeoutTimer: Timer? = null
|
private var connectionTimeoutTimer: Timer? = null
|
||||||
private var hasBeenConnectedOnce = false
|
private var hasBeenConnectedOnce = false
|
||||||
|
|
||||||
private val callStateListener = object : MxCall.StateListener {
|
private val callListener = object : WebRtcCall.Listener {
|
||||||
|
|
||||||
|
override fun onHoldUnhold() {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
isLocalOnHold = call?.isLocalOnHold ?: false,
|
||||||
|
isRemoteOnHold = call?.remoteOnHold ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCaptureStateChanged() {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
isVideoCaptureInError = call?.videoCapturerIsInError ?: false,
|
||||||
|
isHD = call?.currentCaptureFormat() is CaptureFormat.HD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCameraChanged() {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
canSwitchCamera = call?.canSwitchCamera() ?: false,
|
||||||
|
isFrontCamera = call?.currentCameraType() == CameraType.FRONT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTick(formattedDuration: String) {
|
||||||
|
setState {
|
||||||
|
copy(formattedDuration = formattedDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStateUpdate(call: MxCall) {
|
override fun onStateUpdate(call: MxCall) {
|
||||||
val callState = call.state
|
val callState = call.state
|
||||||
if (callState is CallState.Connected && callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) {
|
if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
|
||||||
hasBeenConnectedOnce = true
|
hasBeenConnectedOnce = true
|
||||||
connectionTimeoutTimer?.cancel()
|
connectionTimeoutTimer?.cancel()
|
||||||
connectionTimeoutTimer = null
|
connectionTimeoutTimer = null
|
||||||
|
@ -81,189 +117,182 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
callState = Success(callState)
|
callState = Success(callState),
|
||||||
|
canOpponentBeTransferred = call.capabilities.supportCallTransfer()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val currentCallListener = object : WebRtcPeerConnectionManager.CurrentCallListener {
|
private val currentCallListener = object : WebRtcCallManager.CurrentCallListener {
|
||||||
override fun onCurrentCallChange(call: MxCall?) {
|
|
||||||
// we need to check the state
|
|
||||||
if (call == null) {
|
|
||||||
// we should dismiss, e.g handled by other session?
|
|
||||||
_viewEvents.post(VectorCallViewEvents.DismissNoCall)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCaptureStateChanged() {
|
override fun onCurrentCallChange(call: WebRtcCall?) {
|
||||||
setState {
|
if (call == null) {
|
||||||
copy(
|
_viewEvents.post(VectorCallViewEvents.DismissNoCall)
|
||||||
isVideoCaptureInError = webRtcPeerConnectionManager.capturerIsInError,
|
} else {
|
||||||
isHD = webRtcPeerConnectionManager.currentCaptureFormat() is CaptureFormat.HD
|
updateOtherKnownCall(call)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAudioDevicesChange() {
|
override fun onAudioDevicesChange() {
|
||||||
val currentSoundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice()
|
val currentSoundDevice = callManager.audioManager.selectedDevice ?: return
|
||||||
if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) {
|
if (currentSoundDevice == CallAudioManager.Device.PHONE) {
|
||||||
proximityManager.start()
|
proximityManager.start()
|
||||||
} else {
|
} else {
|
||||||
proximityManager.stop()
|
proximityManager.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
availableSoundDevices = webRtcPeerConnectionManager.callAudioManager.getAvailableSoundDevices(),
|
availableDevices = callManager.audioManager.availableDevices,
|
||||||
soundDevice = currentSoundDevice
|
device = currentSoundDevice
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCameraChange() {
|
private fun updateOtherKnownCall(currentCall: WebRtcCall) {
|
||||||
setState {
|
val otherCall = callManager.getCalls().firstOrNull {
|
||||||
copy(
|
it.callId != currentCall.callId && it.mxCall.state is CallState.Connected
|
||||||
canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera(),
|
}
|
||||||
isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT
|
setState {
|
||||||
)
|
if (otherCall == null) {
|
||||||
|
copy(otherKnownCallInfo = null)
|
||||||
|
} else {
|
||||||
|
val otherUserItem: MatrixItem? = session.getUser(otherCall.mxCall.opponentUserId)?.toMatrixItem()
|
||||||
|
copy(otherKnownCallInfo = VectorCallViewState.CallInfo(otherCall.callId, otherUserItem))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initialState.callId?.let {
|
val webRtcCall = callManager.getCallById(initialState.callId)
|
||||||
webRtcPeerConnectionManager.addCurrentCallListener(currentCallListener)
|
if (webRtcCall == null) {
|
||||||
|
setState {
|
||||||
session.callSignalingService().getCallWithId(it)?.let { mxCall ->
|
copy(callState = Fail(IllegalArgumentException("No call")))
|
||||||
this.call = mxCall
|
|
||||||
mxCall.otherUserId
|
|
||||||
val item: MatrixItem? = session.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()
|
|
||||||
|
|
||||||
mxCall.addListener(callStateListener)
|
|
||||||
|
|
||||||
val currentSoundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice()
|
|
||||||
if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) {
|
|
||||||
proximityManager.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
isVideoCall = mxCall.isVideoCall,
|
|
||||||
callState = Success(mxCall.state),
|
|
||||||
otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized,
|
|
||||||
soundDevice = currentSoundDevice,
|
|
||||||
availableSoundDevices = webRtcPeerConnectionManager.callAudioManager.getAvailableSoundDevices(),
|
|
||||||
isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT,
|
|
||||||
canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera(),
|
|
||||||
isHD = mxCall.isVideoCall && webRtcPeerConnectionManager.currentCaptureFormat() is CaptureFormat.HD
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
callState = Fail(IllegalArgumentException("No call"))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
call = webRtcCall
|
||||||
|
callManager.addCurrentCallListener(currentCallListener)
|
||||||
|
val item: MatrixItem? = session.getUser(webRtcCall.mxCall.opponentUserId)?.toMatrixItem()
|
||||||
|
webRtcCall.addListener(callListener)
|
||||||
|
val currentSoundDevice = callManager.audioManager.selectedDevice
|
||||||
|
if (currentSoundDevice == CallAudioManager.Device.PHONE) {
|
||||||
|
proximityManager.start()
|
||||||
|
}
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
isVideoCall = webRtcCall.mxCall.isVideoCall,
|
||||||
|
callState = Success(webRtcCall.mxCall.state),
|
||||||
|
callInfo = VectorCallViewState.CallInfo(callId, item),
|
||||||
|
device = currentSoundDevice ?: CallAudioManager.Device.PHONE,
|
||||||
|
isLocalOnHold = webRtcCall.isLocalOnHold,
|
||||||
|
isRemoteOnHold = webRtcCall.remoteOnHold,
|
||||||
|
availableDevices = callManager.audioManager.availableDevices,
|
||||||
|
isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT,
|
||||||
|
canSwitchCamera = webRtcCall.canSwitchCamera(),
|
||||||
|
formattedDuration = webRtcCall.formattedDuration(),
|
||||||
|
isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD,
|
||||||
|
canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updateOtherKnownCall(webRtcCall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
// session.callService().removeCallListener(callServiceListener)
|
callManager.removeCurrentCallListener(currentCallListener)
|
||||||
webRtcPeerConnectionManager.removeCurrentCallListener(currentCallListener)
|
call?.removeListener(callListener)
|
||||||
this.call?.removeListener(callStateListener)
|
|
||||||
proximityManager.stop()
|
proximityManager.stop()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: VectorCallViewActions) = withState { state ->
|
override fun handle(action: VectorCallViewActions) = withState { state ->
|
||||||
when (action) {
|
when (action) {
|
||||||
VectorCallViewActions.EndCall -> webRtcPeerConnectionManager.endCall()
|
VectorCallViewActions.EndCall -> call?.endCall()
|
||||||
VectorCallViewActions.AcceptCall -> {
|
VectorCallViewActions.AcceptCall -> {
|
||||||
setState {
|
setState {
|
||||||
copy(callState = Loading())
|
copy(callState = Loading())
|
||||||
}
|
}
|
||||||
webRtcPeerConnectionManager.acceptIncomingCall()
|
call?.acceptIncomingCall()
|
||||||
}
|
}
|
||||||
VectorCallViewActions.DeclineCall -> {
|
VectorCallViewActions.DeclineCall -> {
|
||||||
setState {
|
setState {
|
||||||
copy(callState = Loading())
|
copy(callState = Loading())
|
||||||
}
|
}
|
||||||
webRtcPeerConnectionManager.endCall()
|
call?.endCall()
|
||||||
}
|
}
|
||||||
VectorCallViewActions.ToggleMute -> {
|
VectorCallViewActions.ToggleMute -> {
|
||||||
val muted = state.isAudioMuted
|
val muted = state.isAudioMuted
|
||||||
webRtcPeerConnectionManager.muteCall(!muted)
|
call?.muteCall(!muted)
|
||||||
setState {
|
setState {
|
||||||
copy(isAudioMuted = !muted)
|
copy(isAudioMuted = !muted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
VectorCallViewActions.ToggleVideo -> {
|
VectorCallViewActions.ToggleVideo -> {
|
||||||
if (state.isVideoCall) {
|
if (state.isVideoCall) {
|
||||||
val videoEnabled = state.isVideoEnabled
|
val videoEnabled = state.isVideoEnabled
|
||||||
webRtcPeerConnectionManager.enableVideo(!videoEnabled)
|
call?.enableVideo(!videoEnabled)
|
||||||
setState {
|
setState {
|
||||||
copy(isVideoEnabled = !videoEnabled)
|
copy(isVideoEnabled = !videoEnabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
is VectorCallViewActions.ChangeAudioDevice -> {
|
VectorCallViewActions.ToggleHoldResume -> {
|
||||||
webRtcPeerConnectionManager.callAudioManager.setCurrentSoundDevice(action.device)
|
val isRemoteOnHold = state.isRemoteOnHold
|
||||||
setState {
|
call?.updateRemoteOnHold(!isRemoteOnHold)
|
||||||
copy(
|
|
||||||
soundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
VectorCallViewActions.SwitchSoundDevice -> {
|
is VectorCallViewActions.ChangeAudioDevice -> {
|
||||||
|
callManager.audioManager.setAudioDevice(action.device)
|
||||||
|
}
|
||||||
|
VectorCallViewActions.SwitchSoundDevice -> {
|
||||||
_viewEvents.post(
|
_viewEvents.post(
|
||||||
VectorCallViewEvents.ShowSoundDeviceChooser(state.availableSoundDevices, state.soundDevice)
|
VectorCallViewEvents.ShowSoundDeviceChooser(state.availableDevices, state.device)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
VectorCallViewActions.HeadSetButtonPressed -> {
|
VectorCallViewActions.HeadSetButtonPressed -> {
|
||||||
if (state.callState.invoke() is CallState.LocalRinging) {
|
if (state.callState.invoke() is CallState.LocalRinging) {
|
||||||
// accept call
|
// accept call
|
||||||
webRtcPeerConnectionManager.acceptIncomingCall()
|
call?.acceptIncomingCall()
|
||||||
}
|
}
|
||||||
if (state.callState.invoke() is CallState.Connected) {
|
if (state.callState.invoke() is CallState.Connected) {
|
||||||
// end call?
|
// end call?
|
||||||
webRtcPeerConnectionManager.endCall()
|
call?.endCall()
|
||||||
}
|
}
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
VectorCallViewActions.ToggleCamera -> {
|
VectorCallViewActions.ToggleCamera -> {
|
||||||
webRtcPeerConnectionManager.switchCamera()
|
call?.switchCamera()
|
||||||
}
|
}
|
||||||
VectorCallViewActions.ToggleHDSD -> {
|
VectorCallViewActions.ToggleHDSD -> {
|
||||||
if (!state.isVideoCall) return@withState
|
if (!state.isVideoCall) return@withState
|
||||||
webRtcPeerConnectionManager.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD)
|
call?.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD)
|
||||||
|
}
|
||||||
|
VectorCallViewActions.OpenDialPad -> {
|
||||||
|
_viewEvents.post(VectorCallViewEvents.ShowDialPad)
|
||||||
|
}
|
||||||
|
is VectorCallViewActions.SendDtmfDigit -> {
|
||||||
|
call?.sendDtmfDigit(action.digit)
|
||||||
|
}
|
||||||
|
VectorCallViewActions.InitiateCallTransfer -> {
|
||||||
|
_viewEvents.post(
|
||||||
|
VectorCallViewEvents.ShowCallTransferScreen
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialState: VectorCallViewState, args: CallArgs): VectorCallViewModel
|
fun create(initialState: VectorCallViewState): VectorCallViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<VectorCallViewModel, VectorCallViewState> {
|
companion object : MvRxViewModelFactory<VectorCallViewModel, VectorCallViewState> {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
override fun create(viewModelContext: ViewModelContext, state: VectorCallViewState): VectorCallViewModel? {
|
override fun create(viewModelContext: ViewModelContext, state: VectorCallViewState): VectorCallViewModel {
|
||||||
val callActivity: VectorCallActivity = viewModelContext.activity()
|
val callActivity: VectorCallActivity = viewModelContext.activity()
|
||||||
val callArgs: CallArgs = viewModelContext.args()
|
return callActivity.viewModelFactory.create(state)
|
||||||
return callActivity.viewModelFactory.create(state, callArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun initialState(viewModelContext: ViewModelContext): VectorCallViewState? {
|
|
||||||
val args: CallArgs = viewModelContext.args()
|
|
||||||
return VectorCallViewState(
|
|
||||||
callId = args.callId,
|
|
||||||
roomId = args.roomId,
|
|
||||||
isVideoCall = args.isVideoCall
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,21 +19,39 @@ package im.vector.app.features.call
|
||||||
import com.airbnb.mvrx.Async
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRxState
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
import im.vector.app.features.call.audio.CallAudioManager
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
import org.matrix.android.sdk.api.session.call.CallState
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
|
||||||
data class VectorCallViewState(
|
data class VectorCallViewState(
|
||||||
val callId: String? = null,
|
val callId: String,
|
||||||
val roomId: String = "",
|
val roomId: String,
|
||||||
val isVideoCall: Boolean,
|
val isVideoCall: Boolean,
|
||||||
|
val isRemoteOnHold: Boolean = false,
|
||||||
|
val isLocalOnHold: Boolean = false,
|
||||||
val isAudioMuted: Boolean = false,
|
val isAudioMuted: Boolean = false,
|
||||||
val isVideoEnabled: Boolean = true,
|
val isVideoEnabled: Boolean = true,
|
||||||
val isVideoCaptureInError: Boolean = false,
|
val isVideoCaptureInError: Boolean = false,
|
||||||
val isHD: Boolean = false,
|
val isHD: Boolean = false,
|
||||||
val isFrontCamera: Boolean = true,
|
val isFrontCamera: Boolean = true,
|
||||||
val canSwitchCamera: Boolean = true,
|
val canSwitchCamera: Boolean = true,
|
||||||
val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE,
|
val device: CallAudioManager.Device = CallAudioManager.Device.PHONE,
|
||||||
val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(),
|
val availableDevices: Set<CallAudioManager.Device> = emptySet(),
|
||||||
val otherUserMatrixItem: Async<MatrixItem> = Uninitialized,
|
val callState: Async<CallState> = Uninitialized,
|
||||||
val callState: Async<CallState> = Uninitialized
|
val otherKnownCallInfo: CallInfo? = null,
|
||||||
) : MvRxState
|
val callInfo: CallInfo = CallInfo(callId),
|
||||||
|
val formattedDuration: String = "",
|
||||||
|
val canOpponentBeTransferred: Boolean = false
|
||||||
|
) : MvRxState {
|
||||||
|
|
||||||
|
data class CallInfo(
|
||||||
|
val callId: String,
|
||||||
|
val otherUserItem: MatrixItem? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(callArgs: CallArgs): this(
|
||||||
|
callId = callArgs.callId,
|
||||||
|
roomId = callArgs.roomId,
|
||||||
|
isVideoCall = callArgs.isVideoCall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.
|
||||||
|
*/
|
||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
|
package im.vector.app.features.call.audio
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.bluetooth.BluetoothProfile
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.AudioManager
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import im.vector.app.core.services.BluetoothHeadsetReceiver
|
||||||
|
import im.vector.app.core.services.WiredHeadsetStateReceiver
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.HashSet
|
||||||
|
|
||||||
|
internal class API21AudioDeviceDetector(private val context: Context,
|
||||||
|
private val audioManager: AudioManager,
|
||||||
|
private val callAudioManager: CallAudioManager
|
||||||
|
) : CallAudioManager.AudioDeviceDetector, WiredHeadsetStateReceiver.HeadsetEventListener, BluetoothHeadsetReceiver.EventListener {
|
||||||
|
|
||||||
|
private var bluetoothAdapter: BluetoothAdapter? = null
|
||||||
|
private var connectedBlueToothHeadset: BluetoothProfile? = null
|
||||||
|
private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null
|
||||||
|
private var bluetoothHeadsetStateReceiver: BluetoothHeadsetReceiver? = null
|
||||||
|
|
||||||
|
private val onAudioDeviceChangeRunner = Runnable {
|
||||||
|
val devices = getAvailableSoundDevices()
|
||||||
|
callAudioManager.replaceDevices(devices)
|
||||||
|
Timber.i(" Available audio devices: $devices")
|
||||||
|
callAudioManager.updateAudioRoute()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAvailableSoundDevices(): Set<CallAudioManager.Device> {
|
||||||
|
return HashSet<CallAudioManager.Device>().apply {
|
||||||
|
if (isBluetoothHeadsetOn()) add(CallAudioManager.Device.WIRELESS_HEADSET)
|
||||||
|
if (isWiredHeadsetOn()) {
|
||||||
|
add(CallAudioManager.Device.HEADSET)
|
||||||
|
} else {
|
||||||
|
add(CallAudioManager.Device.PHONE)
|
||||||
|
}
|
||||||
|
add(CallAudioManager.Device.SPEAKER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isWiredHeadsetOn(): Boolean {
|
||||||
|
return audioManager.isWiredHeadsetOn
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isBluetoothHeadsetOn(): Boolean {
|
||||||
|
Timber.v("## VOIP: AudioManager isBluetoothHeadsetOn")
|
||||||
|
try {
|
||||||
|
if (connectedBlueToothHeadset == null) return false.also {
|
||||||
|
Timber.v("## VOIP: AudioManager no connected bluetooth headset")
|
||||||
|
}
|
||||||
|
if (!audioManager.isBluetoothScoAvailableOffCall) return false.also {
|
||||||
|
Timber.v("## VOIP: AudioManager isBluetoothScoAvailableOffCall false")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e("## VOIP: AudioManager isBluetoothHeadsetOn failure ${failure.localizedMessage}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to trigger an audio route update when devices change. It
|
||||||
|
* makes sure the operation is performed on the audio thread.
|
||||||
|
*/
|
||||||
|
private fun onAudioDeviceChange() {
|
||||||
|
callAudioManager.runInAudioThread(onAudioDeviceChangeRunner)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
Timber.i("Start using $this as the audio device handler")
|
||||||
|
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(context, this)
|
||||||
|
bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(context, this)
|
||||||
|
val bm: BluetoothManager? = context.getSystemService()
|
||||||
|
val adapter = bm?.adapter
|
||||||
|
Timber.d("## VOIP Bluetooth adapter $adapter")
|
||||||
|
bluetoothAdapter = adapter
|
||||||
|
adapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
|
override fun onServiceDisconnected(profile: Int) {
|
||||||
|
Timber.d("## VOIP onServiceDisconnected $profile")
|
||||||
|
if (profile == BluetoothProfile.HEADSET) {
|
||||||
|
connectedBlueToothHeadset = null
|
||||||
|
onAudioDeviceChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
|
||||||
|
Timber.d("## VOIP onServiceConnected $profile , proxy:$proxy")
|
||||||
|
if (profile == BluetoothProfile.HEADSET) {
|
||||||
|
connectedBlueToothHeadset = proxy
|
||||||
|
onAudioDeviceChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, BluetoothProfile.HEADSET)
|
||||||
|
onAudioDeviceChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
Timber.i("Stop using $this as the audio device handler")
|
||||||
|
wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(context, it) }
|
||||||
|
wiredHeadsetStateReceiver = null
|
||||||
|
bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(context, it) }
|
||||||
|
bluetoothHeadsetStateReceiver = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
|
||||||
|
Timber.v("onHeadsetEvent $event")
|
||||||
|
onAudioDeviceChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
|
||||||
|
Timber.v("onBTHeadsetEvent $event")
|
||||||
|
onAudioDeviceChange()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.call.audio
|
||||||
|
|
||||||
|
import android.media.AudioDeviceCallback
|
||||||
|
import android.media.AudioDeviceInfo
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.HashSet
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
internal class API23AudioDeviceDetector(private val audioManager: AudioManager,
|
||||||
|
private val callAudioManager: CallAudioManager
|
||||||
|
) : CallAudioManager.AudioDeviceDetector {
|
||||||
|
|
||||||
|
private val onAudioDeviceChangeRunner = Runnable {
|
||||||
|
val devices: MutableSet<CallAudioManager.Device> = HashSet()
|
||||||
|
val deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
||||||
|
for (info in deviceInfos) {
|
||||||
|
when (info.type) {
|
||||||
|
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add(CallAudioManager.Device.WIRELESS_HEADSET)
|
||||||
|
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> devices.add(CallAudioManager.Device.PHONE)
|
||||||
|
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> devices.add(CallAudioManager.Device.SPEAKER)
|
||||||
|
AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_USB_HEADSET -> devices.add(CallAudioManager.Device.HEADSET)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callAudioManager.replaceDevices(devices)
|
||||||
|
Timber.i(" Available audio devices: $devices")
|
||||||
|
callAudioManager.updateAudioRoute()
|
||||||
|
}
|
||||||
|
private val audioDeviceCallback: AudioDeviceCallback = object : AudioDeviceCallback() {
|
||||||
|
override fun onAudioDevicesAdded(
|
||||||
|
addedDevices: Array<AudioDeviceInfo>) {
|
||||||
|
Timber.d(" Audio devices added")
|
||||||
|
onAudioDeviceChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAudioDevicesRemoved(
|
||||||
|
removedDevices: Array<AudioDeviceInfo>) {
|
||||||
|
Timber.d(" Audio devices removed")
|
||||||
|
onAudioDeviceChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to trigger an audio route update when devices change. It
|
||||||
|
* makes sure the operation is performed on the audio thread.
|
||||||
|
*/
|
||||||
|
private fun onAudioDeviceChange() {
|
||||||
|
callAudioManager.runInAudioThread(onAudioDeviceChangeRunner)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
Timber.i("Using $this as the audio device handler")
|
||||||
|
audioManager.registerAudioDeviceCallback(audioDeviceCallback, null)
|
||||||
|
onAudioDeviceChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
audioManager.unregisterAudioDeviceCallback(audioDeviceCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Constant defining a USB headset. Only available on API level >= 26.
|
||||||
|
* The value of: AudioDeviceInfo.TYPE_USB_HEADSET
|
||||||
|
*/
|
||||||
|
private const val TYPE_USB_HEADSET = 22
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,260 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.call.audio
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.HashSet
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
class CallAudioManager(private val context: Context, val configChange: (() -> Unit)?) {
|
||||||
|
|
||||||
|
private val audioManager: AudioManager? = context.getSystemService()
|
||||||
|
private var audioDeviceDetector: AudioDeviceDetector? = null
|
||||||
|
private var audioDeviceRouter: AudioDeviceRouter? = null
|
||||||
|
|
||||||
|
enum class Device {
|
||||||
|
PHONE,
|
||||||
|
SPEAKER,
|
||||||
|
HEADSET,
|
||||||
|
WIRELESS_HEADSET
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Mode {
|
||||||
|
DEFAULT,
|
||||||
|
AUDIO_CALL,
|
||||||
|
VIDEO_CALL
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mode = Mode.DEFAULT
|
||||||
|
private var _availableDevices: MutableSet<Device> = HashSet()
|
||||||
|
val availableDevices: Set<Device>
|
||||||
|
get() = _availableDevices
|
||||||
|
|
||||||
|
var selectedDevice: Device? = null
|
||||||
|
private set
|
||||||
|
private var userSelectedDevice: Device? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
runInAudioThread { setup() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setup() {
|
||||||
|
if (audioManager == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audioDeviceDetector?.stop()
|
||||||
|
audioDeviceDetector = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
API23AudioDeviceDetector(audioManager, this)
|
||||||
|
} else {
|
||||||
|
API21AudioDeviceDetector(context, audioManager, this)
|
||||||
|
}
|
||||||
|
audioDeviceDetector?.start()
|
||||||
|
audioDeviceRouter = DefaultAudioDeviceRouter(audioManager, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runInAudioThread(runnable: Runnable) {
|
||||||
|
executor.execute(runnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the user selected audio device as the active audio device.
|
||||||
|
*
|
||||||
|
* @param device the desired device which will become active.
|
||||||
|
*/
|
||||||
|
fun setAudioDevice(device: Device) {
|
||||||
|
runInAudioThread(Runnable {
|
||||||
|
if (!_availableDevices.contains(device)) {
|
||||||
|
Timber.w(" Audio device not available: $device")
|
||||||
|
userSelectedDevice = null
|
||||||
|
return@Runnable
|
||||||
|
}
|
||||||
|
if (mode != Mode.DEFAULT) {
|
||||||
|
Timber.i(" User selected device set to: $device")
|
||||||
|
userSelectedDevice = device
|
||||||
|
updateAudioRoute(mode, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public method to set the current audio mode.
|
||||||
|
*
|
||||||
|
* @param mode the desired audio mode.
|
||||||
|
* could be updated successfully, and it will be rejected otherwise.
|
||||||
|
*/
|
||||||
|
fun setMode(mode: Mode) {
|
||||||
|
runInAudioThread {
|
||||||
|
var success: Boolean
|
||||||
|
try {
|
||||||
|
success = updateAudioRoute(mode, false)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
success = false
|
||||||
|
Timber.e(e, " Failed to update audio route for mode: " + mode)
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
this@CallAudioManager.mode = mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the audio route for the given mode.
|
||||||
|
*
|
||||||
|
* @param mode the audio mode to be used when computing the audio route.
|
||||||
|
* @return `true` if the audio route was updated successfully;
|
||||||
|
* `false`, otherwise.
|
||||||
|
*/
|
||||||
|
private fun updateAudioRoute(mode: Mode, force: Boolean): Boolean {
|
||||||
|
Timber.i(" Update audio route for mode: " + mode)
|
||||||
|
if (!audioDeviceRouter?.setMode(mode).orFalse()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (mode == Mode.DEFAULT) {
|
||||||
|
selectedDevice = null
|
||||||
|
userSelectedDevice = null
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val bluetoothAvailable = _availableDevices.contains(Device.WIRELESS_HEADSET)
|
||||||
|
val headsetAvailable = _availableDevices.contains(Device.HEADSET)
|
||||||
|
|
||||||
|
// Pick the desired device based on what's available and the mode.
|
||||||
|
var audioDevice: Device
|
||||||
|
audioDevice = if (bluetoothAvailable) {
|
||||||
|
Device.WIRELESS_HEADSET
|
||||||
|
} else if (headsetAvailable) {
|
||||||
|
Device.HEADSET
|
||||||
|
} else if (mode == Mode.VIDEO_CALL) {
|
||||||
|
Device.SPEAKER
|
||||||
|
} else {
|
||||||
|
Device.PHONE
|
||||||
|
}
|
||||||
|
// Consider the user's selection
|
||||||
|
if (userSelectedDevice != null && _availableDevices.contains(userSelectedDevice)) {
|
||||||
|
audioDevice = userSelectedDevice!!
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the previously selected device and the current default one
|
||||||
|
// match, do nothing.
|
||||||
|
if (!force && selectedDevice != null && selectedDevice == audioDevice) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
selectedDevice = audioDevice
|
||||||
|
Timber.i(" Selected audio device: " + audioDevice)
|
||||||
|
audioDeviceRouter?.setAudioRoute(audioDevice)
|
||||||
|
configChange?.invoke()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the current device selection.
|
||||||
|
*/
|
||||||
|
fun resetSelectedDevice() {
|
||||||
|
selectedDevice = null
|
||||||
|
userSelectedDevice = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new device to the list of available devices.
|
||||||
|
*
|
||||||
|
* @param device The new device.
|
||||||
|
*/
|
||||||
|
fun addDevice(device: Device) {
|
||||||
|
_availableDevices.add(device)
|
||||||
|
resetSelectedDevice()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a device from the list of available devices.
|
||||||
|
*
|
||||||
|
* @param device The old device to the removed.
|
||||||
|
*/
|
||||||
|
fun removeDevice(device: Device) {
|
||||||
|
_availableDevices.remove(device)
|
||||||
|
resetSelectedDevice()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the current list of available devices with a new one.
|
||||||
|
*
|
||||||
|
* @param devices The new devices list.
|
||||||
|
*/
|
||||||
|
fun replaceDevices(devices: Set<Device>) {
|
||||||
|
_availableDevices.clear()
|
||||||
|
_availableDevices.addAll(devices)
|
||||||
|
resetSelectedDevice()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-sets the current audio route. Needed when devices changes have happened.
|
||||||
|
*/
|
||||||
|
fun updateAudioRoute() {
|
||||||
|
if (mode != Mode.DEFAULT) {
|
||||||
|
updateAudioRoute(mode, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-sets the current audio route. Needed when focus is lost and regained.
|
||||||
|
*/
|
||||||
|
fun resetAudioRoute() {
|
||||||
|
if (mode != Mode.DEFAULT) {
|
||||||
|
updateAudioRoute(mode, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for the modules implementing the actual audio device management.
|
||||||
|
*/
|
||||||
|
interface AudioDeviceDetector {
|
||||||
|
/**
|
||||||
|
* Start detecting audio device changes.
|
||||||
|
*/
|
||||||
|
fun start()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop audio device detection.
|
||||||
|
*/
|
||||||
|
fun stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AudioDeviceRouter {
|
||||||
|
/**
|
||||||
|
* Set the appropriate route for the given audio device.
|
||||||
|
*
|
||||||
|
* @param device Audio device for which the route must be set.
|
||||||
|
*/
|
||||||
|
fun setAudioRoute(device: Device)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the given audio mode.
|
||||||
|
*
|
||||||
|
* @param mode The new audio mode to be used.
|
||||||
|
* @return Whether the operation was successful or not.
|
||||||
|
*/
|
||||||
|
fun setMode(mode: Mode): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Every audio operations should be launched on single thread
|
||||||
|
private val executor = Executors.newSingleThreadExecutor()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.call.audio
|
||||||
|
|
||||||
|
import android.media.AudioManager
|
||||||
|
import androidx.media.AudioAttributesCompat
|
||||||
|
import androidx.media.AudioFocusRequestCompat
|
||||||
|
import androidx.media.AudioManagerCompat
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class DefaultAudioDeviceRouter(private val audioManager: AudioManager,
|
||||||
|
private val callAudioManager: CallAudioManager
|
||||||
|
) : CallAudioManager.AudioDeviceRouter, AudioManager.OnAudioFocusChangeListener {
|
||||||
|
|
||||||
|
private var audioFocusLost = false
|
||||||
|
|
||||||
|
private var focusRequestCompat: AudioFocusRequestCompat? = null
|
||||||
|
|
||||||
|
override fun setAudioRoute(device: CallAudioManager.Device) {
|
||||||
|
audioManager.isSpeakerphoneOn = device === CallAudioManager.Device.SPEAKER
|
||||||
|
setBluetoothAudioRoute(device === CallAudioManager.Device.WIRELESS_HEADSET)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMode(mode: CallAudioManager.Mode): Boolean {
|
||||||
|
if (mode === CallAudioManager.Mode.DEFAULT) {
|
||||||
|
audioFocusLost = false
|
||||||
|
audioManager.mode = AudioManager.MODE_NORMAL
|
||||||
|
focusRequestCompat?.also {
|
||||||
|
AudioManagerCompat.abandonAudioFocusRequest(audioManager, it)
|
||||||
|
}
|
||||||
|
focusRequestCompat = null
|
||||||
|
audioManager.isSpeakerphoneOn = false
|
||||||
|
setBluetoothAudioRoute(false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||||
|
audioManager.isMicrophoneMute = false
|
||||||
|
|
||||||
|
val audioFocusRequest = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
|
||||||
|
.setAudioAttributes(
|
||||||
|
AudioAttributesCompat.Builder()
|
||||||
|
.setUsage(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION)
|
||||||
|
.setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.setOnAudioFocusChangeListener(this)
|
||||||
|
.build()
|
||||||
|
.also {
|
||||||
|
focusRequestCompat = it
|
||||||
|
}
|
||||||
|
|
||||||
|
val gotFocus = AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest)
|
||||||
|
if (gotFocus == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
|
||||||
|
Timber.w(" Audio focus request failed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to set the output route to a Bluetooth device.
|
||||||
|
*
|
||||||
|
* @param enabled true if Bluetooth should use used, false otherwise.
|
||||||
|
*/
|
||||||
|
private fun setBluetoothAudioRoute(enabled: Boolean) {
|
||||||
|
if (enabled) {
|
||||||
|
audioManager.startBluetoothSco()
|
||||||
|
audioManager.isBluetoothScoOn = true
|
||||||
|
} else {
|
||||||
|
audioManager.isBluetoothScoOn = false
|
||||||
|
audioManager.stopBluetoothSco()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [AudioManager.OnAudioFocusChangeListener] interface method. Called
|
||||||
|
* when the audio focus of the system is updated.
|
||||||
|
*
|
||||||
|
* @param focusChange - The type of focus change.
|
||||||
|
*/
|
||||||
|
override fun onAudioFocusChange(focusChange: Int) {
|
||||||
|
callAudioManager.runInAudioThread {
|
||||||
|
when (focusChange) {
|
||||||
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||||
|
Timber.d(" Audio focus gained")
|
||||||
|
if (audioFocusLost) {
|
||||||
|
callAudioManager.resetAudioRoute()
|
||||||
|
}
|
||||||
|
audioFocusLost = false
|
||||||
|
}
|
||||||
|
AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||||
|
Timber.d(" Audio focus lost")
|
||||||
|
audioFocusLost = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.call.dialpad
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.extensions.addChildFragment
|
||||||
|
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||||
|
import im.vector.app.databinding.BottomSheetCallDialPadBinding
|
||||||
|
import im.vector.app.features.settings.VectorLocale
|
||||||
|
|
||||||
|
class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCallDialPadBinding>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val EXTRA_SHOW_ACTIONS = "EXTRA_SHOW_ACTIONS"
|
||||||
|
|
||||||
|
fun newInstance(showActions: Boolean): CallDialPadBottomSheet {
|
||||||
|
return CallDialPadBottomSheet().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putBoolean(EXTRA_SHOW_ACTIONS, showActions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val showExpanded = true
|
||||||
|
|
||||||
|
var callback: DialPadFragment.Callback? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
setCallbackToFragment(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetCallDialPadBinding {
|
||||||
|
return BottomSheetCallDialPadBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
val showActions = arguments?.getBoolean(EXTRA_SHOW_ACTIONS, false) ?: false
|
||||||
|
DialPadFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, showActions)
|
||||||
|
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, showActions)
|
||||||
|
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
|
||||||
|
}
|
||||||
|
callback = DialPadFragmentCallbackWrapper(this@CallDialPadBottomSheet.callback)
|
||||||
|
}.also {
|
||||||
|
addChildFragment(R.id.callDialPadFragmentContainer, it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setCallbackToFragment(callback)
|
||||||
|
}
|
||||||
|
views.callDialPadClose.setOnClickListener {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
setCallbackToFragment(null)
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setCallbackToFragment(callback: DialPadFragment.Callback?) {
|
||||||
|
if (!isAdded) return
|
||||||
|
val dialPadFragment = childFragmentManager.findFragmentById(R.id.callDialPadFragmentContainer) as? DialPadFragment
|
||||||
|
dialPadFragment?.callback = DialPadFragmentCallbackWrapper(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class DialPadFragmentCallbackWrapper(val callback: DialPadFragment.Callback?): DialPadFragment.Callback {
|
||||||
|
|
||||||
|
override fun onDigitAppended(digit: String) {
|
||||||
|
callback?.onDigitAppended(digit)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOkClicked(formatted: String?, raw: String?) {
|
||||||
|
callback?.onOkClicked(formatted, raw)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.call.dialpad
|
||||||
|
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.widget.ImageViewCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.android.dialer.dialpadview.DialpadView
|
||||||
|
import com.android.dialer.dialpadview.DigitsEditText
|
||||||
|
import com.android.dialer.dialpadview.R
|
||||||
|
import com.google.i18n.phonenumbers.AsYouTypeFormatter
|
||||||
|
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||||
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
|
|
||||||
|
class DialPadFragment : Fragment() {
|
||||||
|
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
|
private var digits: DigitsEditText? = null
|
||||||
|
private var formatter: AsYouTypeFormatter? = null
|
||||||
|
private var input = ""
|
||||||
|
private var regionCode: String = DEFAULT_REGION_CODE
|
||||||
|
private var formatAsYouType = true
|
||||||
|
private var enableStar = true
|
||||||
|
private var enablePound = true
|
||||||
|
private var enablePlus = true
|
||||||
|
private var cursorVisible = false
|
||||||
|
private var enableDelete = true
|
||||||
|
private var enableFabOk = true
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View {
|
||||||
|
initArgs(savedInstanceState)
|
||||||
|
val view = inflater.inflate(R.layout.dialpad_fragment, container, false)
|
||||||
|
val dialpadView = view.findViewById<View>(R.id.dialpad_view) as DialpadView
|
||||||
|
dialpadView.findViewById<View>(R.id.dialpad_key_voicemail).isVisible = false
|
||||||
|
digits = dialpadView.digits as? DigitsEditText
|
||||||
|
digits?.isCursorVisible = cursorVisible
|
||||||
|
digits?.setTextColor(ThemeUtils.getColor(requireContext(), im.vector.app.R.attr.riotx_text_primary))
|
||||||
|
dialpadView.findViewById<View>(R.id.zero).setOnClickListener { append('0') }
|
||||||
|
if (enablePlus) {
|
||||||
|
dialpadView.findViewById<View>(R.id.zero).setOnLongClickListener {
|
||||||
|
append('+')
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialpadView.findViewById<View>(R.id.one).setOnClickListener { append('1') }
|
||||||
|
dialpadView.findViewById<View>(R.id.two).setOnClickListener { append('2') }
|
||||||
|
dialpadView.findViewById<View>(R.id.three).setOnClickListener { append('3') }
|
||||||
|
dialpadView.findViewById<View>(R.id.four).setOnClickListener { append('4') }
|
||||||
|
dialpadView.findViewById<View>(R.id.four).setOnClickListener { append('4') }
|
||||||
|
dialpadView.findViewById<View>(R.id.five).setOnClickListener { append('5') }
|
||||||
|
dialpadView.findViewById<View>(R.id.six).setOnClickListener { append('6') }
|
||||||
|
dialpadView.findViewById<View>(R.id.seven).setOnClickListener { append('7') }
|
||||||
|
dialpadView.findViewById<View>(R.id.eight).setOnClickListener { append('8') }
|
||||||
|
dialpadView.findViewById<View>(R.id.nine).setOnClickListener { append('9') }
|
||||||
|
if (enableStar) {
|
||||||
|
dialpadView.findViewById<View>(R.id.star).setOnClickListener { append('*') }
|
||||||
|
} else {
|
||||||
|
dialpadView.findViewById<View>(R.id.star).isVisible = false
|
||||||
|
}
|
||||||
|
if (enablePound) {
|
||||||
|
dialpadView.findViewById<View>(R.id.pound).setOnClickListener { append('#') }
|
||||||
|
} else {
|
||||||
|
dialpadView.findViewById<View>(R.id.pound).isVisible = false
|
||||||
|
}
|
||||||
|
if (enableDelete) {
|
||||||
|
dialpadView.deleteButton.setOnClickListener { poll() }
|
||||||
|
dialpadView.deleteButton.setOnLongClickListener {
|
||||||
|
clear()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
val tintColor = ThemeUtils.getColor(requireContext(), im.vector.app.R.attr.riotx_text_secondary)
|
||||||
|
ImageViewCompat.setImageTintList(dialpadView.deleteButton, ColorStateList.valueOf(tintColor))
|
||||||
|
} else {
|
||||||
|
dialpadView.deleteButton.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// if region code is null, no formatting is performed
|
||||||
|
formatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(if (formatAsYouType) regionCode else "")
|
||||||
|
|
||||||
|
val fabOk = view.findViewById<View>(R.id.fab_ok)
|
||||||
|
if (enableFabOk) {
|
||||||
|
fabOk.setOnClickListener {
|
||||||
|
callback?.onOkClicked(digits?.text.toString(), input)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fabOk.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
digits?.setOnTextContextMenuClickListener {
|
||||||
|
val string = digits?.text.toString()
|
||||||
|
clear()
|
||||||
|
for (element in string) {
|
||||||
|
append(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putString(EXTRA_REGION_CODE, regionCode)
|
||||||
|
outState.putBoolean(EXTRA_FORMAT_AS_YOU_TYPE, formatAsYouType)
|
||||||
|
outState.putBoolean(EXTRA_ENABLE_STAR, enableStar)
|
||||||
|
outState.putBoolean(EXTRA_ENABLE_POUND, enablePound)
|
||||||
|
outState.putBoolean(EXTRA_ENABLE_PLUS, enablePlus)
|
||||||
|
outState.putBoolean(EXTRA_ENABLE_OK, enableFabOk)
|
||||||
|
outState.putBoolean(EXTRA_ENABLE_DELETE, enableDelete)
|
||||||
|
outState.putBoolean(EXTRA_CURSOR_VISIBLE, cursorVisible)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initArgs(savedInstanceState: Bundle?) {
|
||||||
|
val args = savedInstanceState ?: arguments
|
||||||
|
if (args != null) {
|
||||||
|
regionCode = args.getString(EXTRA_REGION_CODE, DEFAULT_REGION_CODE)
|
||||||
|
formatAsYouType = args.getBoolean(EXTRA_FORMAT_AS_YOU_TYPE, formatAsYouType)
|
||||||
|
enableStar = args.getBoolean(EXTRA_ENABLE_STAR, enableStar)
|
||||||
|
enablePound = args.getBoolean(EXTRA_ENABLE_POUND, enablePound)
|
||||||
|
enablePlus = args.getBoolean(EXTRA_ENABLE_PLUS, enablePlus)
|
||||||
|
enableDelete = args.getBoolean(EXTRA_ENABLE_DELETE, enableDelete)
|
||||||
|
enableFabOk = args.getBoolean(EXTRA_ENABLE_OK, enableFabOk)
|
||||||
|
cursorVisible = args.getBoolean(EXTRA_CURSOR_VISIBLE, cursorVisible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun poll() {
|
||||||
|
if (!input.isEmpty()) {
|
||||||
|
input = input.substring(0, input.length - 1)
|
||||||
|
formatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(regionCode)
|
||||||
|
if (formatAsYouType) {
|
||||||
|
digits?.setText("")
|
||||||
|
for (c in input.toCharArray()) {
|
||||||
|
digits?.setText(formatter?.inputDigit(c))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
digits?.setText(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clear() {
|
||||||
|
formatter?.clear()
|
||||||
|
digits?.setText("")
|
||||||
|
input = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun append(c: Char) {
|
||||||
|
callback?.onDigitAppended(c.toString())
|
||||||
|
input += c
|
||||||
|
if (formatAsYouType) {
|
||||||
|
digits?.setText(formatter?.inputDigit(c))
|
||||||
|
} else {
|
||||||
|
digits?.setText(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onOkClicked(formatted: String?, raw: String?) = Unit
|
||||||
|
fun onDigitAppended(digit: String) = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val EXTRA_REGION_CODE = "EXTRA_REGION_CODE"
|
||||||
|
const val EXTRA_FORMAT_AS_YOU_TYPE = "EXTRA_FORMAT_AS_YOU_TYPE"
|
||||||
|
const val EXTRA_ENABLE_STAR = "EXTRA_ENABLE_STAR"
|
||||||
|
const val EXTRA_ENABLE_POUND = "EXTRA_ENABLE_POUND"
|
||||||
|
const val EXTRA_ENABLE_PLUS = "EXTRA_ENABLE_PLUS"
|
||||||
|
const val EXTRA_ENABLE_DELETE = "EXTRA_ENABLE_DELETE"
|
||||||
|
const val EXTRA_ENABLE_OK = "EXTRA_ENABLE_OK"
|
||||||
|
const val EXTRA_CURSOR_VISIBLE = "EXTRA_CURSOR_VISIBLE"
|
||||||
|
|
||||||
|
private const val DEFAULT_REGION_CODE = "US"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.call.dialpad
|
||||||
|
|
||||||
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
|
import im.vector.app.features.createdirect.DirectRoomHelper
|
||||||
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DialPadLookup @Inject constructor(val session: Session,
|
||||||
|
val directRoomHelper: DirectRoomHelper,
|
||||||
|
val callManager: WebRtcCallManager
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Failure : Throwable()
|
||||||
|
data class Result(val userId: String, val roomId: String)
|
||||||
|
|
||||||
|
suspend fun lookupPhoneNumber(phoneNumber: String): Result {
|
||||||
|
val supportedProtocolKey = callManager.supportedPSTNProtocol ?: throw Failure()
|
||||||
|
val thirdPartyUser = tryOrNull {
|
||||||
|
session.thirdPartyService().getThirdPartyUser(supportedProtocolKey, fields = mapOf(
|
||||||
|
"m.id.phone" to phoneNumber
|
||||||
|
)).firstOrNull()
|
||||||
|
} ?: throw Failure()
|
||||||
|
|
||||||
|
val roomId = directRoomHelper.ensureDMExists(thirdPartyUser.userId)
|
||||||
|
return Result(userId = thirdPartyUser.userId, roomId = roomId)
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,24 +20,28 @@ import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import im.vector.app.core.di.HasVectorInjector
|
import im.vector.app.core.di.HasVectorInjector
|
||||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class CallHeadsUpActionReceiver : BroadcastReceiver() {
|
class CallHeadsUpActionReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val EXTRA_CALL_ACTION_KEY = "EXTRA_CALL_ACTION_KEY"
|
const val EXTRA_CALL_ACTION_KEY = "EXTRA_CALL_ACTION_KEY"
|
||||||
|
const val EXTRA_CALL_ID = "EXTRA_CALL_ID"
|
||||||
const val CALL_ACTION_REJECT = 0
|
const val CALL_ACTION_REJECT = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
val peerConnectionManager = (context.applicationContext as? HasVectorInjector)
|
val webRtcCallManager = (context.applicationContext as? HasVectorInjector)
|
||||||
?.injector()
|
?.injector()
|
||||||
?.webRtcPeerConnectionManager()
|
?.webRtcCallManager()
|
||||||
?: return
|
?: return
|
||||||
|
|
||||||
when (intent?.getIntExtra(EXTRA_CALL_ACTION_KEY, 0)) {
|
when (intent?.getIntExtra(EXTRA_CALL_ACTION_KEY, 0)) {
|
||||||
CALL_ACTION_REJECT -> onCallRejectClicked(peerConnectionManager)
|
CALL_ACTION_REJECT -> {
|
||||||
|
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return
|
||||||
|
onCallRejectClicked(webRtcCallManager, callId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not sure why this should be needed
|
// Not sure why this should be needed
|
||||||
|
@ -48,9 +52,9 @@ class CallHeadsUpActionReceiver : BroadcastReceiver() {
|
||||||
// context.stopService(Intent(context, CallHeadsUpService::class.java))
|
// context.stopService(Intent(context, CallHeadsUpService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCallRejectClicked(peerConnectionManager: WebRtcPeerConnectionManager) {
|
private fun onCallRejectClicked(callManager: WebRtcCallManager, callId: String) {
|
||||||
Timber.d("onCallRejectClicked")
|
Timber.d("onCallRejectClicked")
|
||||||
peerConnectionManager.endCall()
|
callManager.getCallById(callId)?.endCall()
|
||||||
}
|
}
|
||||||
|
|
||||||
// private fun onCallAnswerClicked(context: Context) {
|
// private fun onCallAnswerClicked(context: Context) {
|
||||||
|
|
|
@ -21,7 +21,7 @@ import android.os.Build
|
||||||
import android.telecom.Connection
|
import android.telecom.Connection
|
||||||
import android.telecom.DisconnectCause
|
import android.telecom.DisconnectCause
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ import javax.inject.Inject
|
||||||
val callId: String
|
val callId: String
|
||||||
) : Connection() {
|
) : Connection() {
|
||||||
|
|
||||||
@Inject lateinit var peerConnectionManager: WebRtcPeerConnectionManager
|
@Inject lateinit var callManager: WebRtcCallManager
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.call.telecom;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
|
import org.jitsi.meet.sdk.ConnectionService;
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
public class CallConnectionService extends ConnectionService {
|
||||||
|
}
|
|
@ -71,7 +71,7 @@ import im.vector.app.core.services.CallService
|
||||||
|
|
||||||
bindService(Intent(applicationContext, CallService::class.java), CallServiceConnection(connection), 0)
|
bindService(Intent(applicationContext, CallService::class.java), CallServiceConnection(connection), 0)
|
||||||
connection.setInitializing()
|
connection.setInitializing()
|
||||||
return CallConnection(applicationContext, roomId, callId)
|
return connection
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class CallServiceConnection(private val callConnection: CallConnection) : ServiceConnection {
|
inner class CallServiceConnection(private val callConnection: CallConnection) : ServiceConnection {
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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.app.features.call.transfer
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
|
sealed class CallTransferAction : VectorViewModelAction {
|
||||||
|
data class ConnectWithUserId(val consultFirst: Boolean, val selectedUserId: String) : CallTransferAction()
|
||||||
|
data class ConnectWithPhoneNumber(val consultFirst: Boolean, val phoneNumber: String) : CallTransferAction()
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
* 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.app.features.call.transfer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.airbnb.mvrx.MvRx
|
||||||
|
import com.airbnb.mvrx.viewModel
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.di.ScreenComponent
|
||||||
|
import im.vector.app.core.error.ErrorFormatter
|
||||||
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
|
import im.vector.app.databinding.ActivityCallTransferBinding
|
||||||
|
import im.vector.app.features.contactsbook.ContactsBookViewModel
|
||||||
|
import im.vector.app.features.contactsbook.ContactsBookViewState
|
||||||
|
import im.vector.app.features.userdirectory.UserListViewModel
|
||||||
|
import im.vector.app.features.userdirectory.UserListViewState
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class CallTransferArgs(val callId: String) : Parcelable
|
||||||
|
|
||||||
|
private const val USER_LIST_FRAGMENT_TAG = "USER_LIST_FRAGMENT_TAG"
|
||||||
|
|
||||||
|
class CallTransferActivity : VectorBaseActivity<ActivityCallTransferBinding>(),
|
||||||
|
CallTransferViewModel.Factory,
|
||||||
|
UserListViewModel.Factory,
|
||||||
|
ContactsBookViewModel.Factory {
|
||||||
|
|
||||||
|
@Inject lateinit var userListViewModelFactory: UserListViewModel.Factory
|
||||||
|
@Inject lateinit var callTransferViewModelFactory: CallTransferViewModel.Factory
|
||||||
|
@Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
|
||||||
|
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||||
|
|
||||||
|
private lateinit var sectionsPagerAdapter: CallTransferPagerAdapter
|
||||||
|
|
||||||
|
private val callTransferViewModel: CallTransferViewModel by viewModel()
|
||||||
|
|
||||||
|
override fun getBinding() = ActivityCallTransferBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
override fun getCoordinatorLayout() = views.vectorCoordinatorLayout
|
||||||
|
|
||||||
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
super.injectWith(injector)
|
||||||
|
injector.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(initialState: UserListViewState): UserListViewModel {
|
||||||
|
return userListViewModelFactory.create(initialState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(initialState: CallTransferViewState): CallTransferViewModel {
|
||||||
|
return callTransferViewModelFactory.create(initialState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(initialState: ContactsBookViewState): ContactsBookViewModel {
|
||||||
|
return contactsBookViewModelFactory.create(initialState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
waitingView = views.waitingView.waitingView
|
||||||
|
|
||||||
|
callTransferViewModel.observeViewEvents {
|
||||||
|
when (it) {
|
||||||
|
is CallTransferViewEvents.Dismiss -> finish()
|
||||||
|
CallTransferViewEvents.Loading -> showWaitingView()
|
||||||
|
is CallTransferViewEvents.FailToTransfer -> showSnackbar(getString(R.string.call_transfer_failure))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionsPagerAdapter = CallTransferPagerAdapter(this).register()
|
||||||
|
views.callTransferViewPager.adapter = sectionsPagerAdapter
|
||||||
|
sectionsPagerAdapter.onDialPadOkClicked = { phoneNumber ->
|
||||||
|
val action = CallTransferAction.ConnectWithPhoneNumber(views.callTransferConsultCheckBox.isChecked, phoneNumber)
|
||||||
|
callTransferViewModel.handle(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
TabLayoutMediator(views.callTransferTabLayout, views.callTransferViewPager) { tab, position ->
|
||||||
|
when (position) {
|
||||||
|
0 -> tab.text = getString(R.string.call_transfer_users_tab_title)
|
||||||
|
1 -> tab.text = getString(R.string.call_dial_pad_title)
|
||||||
|
}
|
||||||
|
}.attach()
|
||||||
|
configureToolbar(views.callTransferToolbar)
|
||||||
|
views.callTransferToolbar.title = getString(R.string.call_transfer_title)
|
||||||
|
setupConnectAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupConnectAction() {
|
||||||
|
views.callTransferConnectAction.debouncedClicks {
|
||||||
|
val selectedUser = sectionsPagerAdapter.userListFragment?.getCurrentState()?.getSelectedMatrixId()?.firstOrNull()
|
||||||
|
if (selectedUser != null) {
|
||||||
|
val action = CallTransferAction.ConnectWithUserId(views.callTransferConsultCheckBox.isChecked, selectedUser)
|
||||||
|
callTransferViewModel.handle(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun newIntent(context: Context, callId: String): Intent {
|
||||||
|
return Intent(context, CallTransferActivity::class.java).also {
|
||||||
|
it.putExtra(MvRx.KEY_ARG, CallTransferArgs(callId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.call.transfer
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import im.vector.app.core.extensions.toMvRxBundle
|
||||||
|
import im.vector.app.core.platform.Restorable
|
||||||
|
import im.vector.app.features.call.dialpad.DialPadFragment
|
||||||
|
import im.vector.app.features.settings.VectorLocale
|
||||||
|
import im.vector.app.features.userdirectory.UserListFragment
|
||||||
|
import im.vector.app.features.userdirectory.UserListFragmentArgs
|
||||||
|
|
||||||
|
class CallTransferPagerAdapter(
|
||||||
|
private val fragmentActivity: FragmentActivity
|
||||||
|
) : FragmentStateAdapter(fragmentActivity), Restorable {
|
||||||
|
|
||||||
|
val userListFragment: UserListFragment?
|
||||||
|
get() = findFragmentAtPosition(0) as? UserListFragment
|
||||||
|
val dialPadFragment: DialPadFragment?
|
||||||
|
get() = findFragmentAtPosition(1) as? DialPadFragment
|
||||||
|
|
||||||
|
var onDialPadOkClicked: ((String) -> Unit)? = null
|
||||||
|
|
||||||
|
override fun getItemCount() = 2
|
||||||
|
|
||||||
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
val fragment: Fragment
|
||||||
|
if (position == 0) {
|
||||||
|
fragment = fragmentActivity.supportFragmentManager.fragmentFactory.instantiate(fragmentActivity.classLoader, UserListFragment::class.java.name)
|
||||||
|
fragment.arguments = UserListFragmentArgs(
|
||||||
|
title = "",
|
||||||
|
menuResId = -1,
|
||||||
|
singleSelection = true,
|
||||||
|
showInviteActions = false,
|
||||||
|
showToolbar = false,
|
||||||
|
showContactBookAction = false
|
||||||
|
).toMvRxBundle()
|
||||||
|
} else {
|
||||||
|
fragment = fragmentActivity.supportFragmentManager.fragmentFactory.instantiate(fragmentActivity.classLoader, DialPadFragment::class.java.name)
|
||||||
|
(fragment as DialPadFragment).apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
|
||||||
|
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true)
|
||||||
|
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
|
||||||
|
}
|
||||||
|
applyCallback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findFragmentAtPosition(position: Int): Fragment? {
|
||||||
|
return fragmentActivity.supportFragmentManager.findFragmentByTag("f$position")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) = Unit
|
||||||
|
|
||||||
|
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
|
||||||
|
dialPadFragment?.applyCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DialPadFragment.applyCallback(): DialPadFragment {
|
||||||
|
callback = object : DialPadFragment.Callback {
|
||||||
|
override fun onOkClicked(formatted: String?, raw: String?) {
|
||||||
|
if (raw.isNullOrEmpty()) return
|
||||||
|
onDialPadOkClicked?.invoke(raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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.app.features.call.transfer
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
|
||||||
|
sealed class CallTransferViewEvents : VectorViewEvents {
|
||||||
|
object Dismiss : CallTransferViewEvents()
|
||||||
|
object Loading: CallTransferViewEvents()
|
||||||
|
object FailToTransfer : CallTransferViewEvents()
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
* 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.app.features.call.transfer
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.airbnb.mvrx.ActivityViewModelContext
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.core.extensions.exhaustive
|
||||||
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import im.vector.app.features.call.dialpad.DialPadLookup
|
||||||
|
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||||
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.session.call.CallState
|
||||||
|
import org.matrix.android.sdk.api.session.call.MxCall
|
||||||
|
|
||||||
|
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
|
||||||
|
private val dialPadLookup: DialPadLookup,
|
||||||
|
callManager: WebRtcCallManager)
|
||||||
|
: VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) {
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: CallTransferViewState): CallTransferViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<CallTransferViewModel, CallTransferViewState> {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: CallTransferViewState): CallTransferViewModel? {
|
||||||
|
val activity: CallTransferActivity = (viewModelContext as ActivityViewModelContext).activity()
|
||||||
|
return activity.callTransferViewModelFactory.create(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val call = callManager.getCallById(initialState.callId)
|
||||||
|
private val callListener = object : WebRtcCall.Listener {
|
||||||
|
override fun onStateUpdate(call: MxCall) {
|
||||||
|
if (call.state == CallState.Terminated) {
|
||||||
|
_viewEvents.post(CallTransferViewEvents.Dismiss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (call == null) {
|
||||||
|
_viewEvents.post(CallTransferViewEvents.Dismiss)
|
||||||
|
} else {
|
||||||
|
call.addListener(callListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
call?.removeListener(callListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: CallTransferAction) {
|
||||||
|
when (action) {
|
||||||
|
is CallTransferAction.ConnectWithUserId -> connectWithUserId(action)
|
||||||
|
is CallTransferAction.ConnectWithPhoneNumber -> connectWithPhoneNumber(action)
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
_viewEvents.post(CallTransferViewEvents.Loading)
|
||||||
|
call?.mxCall?.transfer(action.selectedUserId, null)
|
||||||
|
_viewEvents.post(CallTransferViewEvents.Dismiss)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
_viewEvents.post(CallTransferViewEvents.FailToTransfer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectWithPhoneNumber(action: CallTransferAction.ConnectWithPhoneNumber) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
_viewEvents.post(CallTransferViewEvents.Loading)
|
||||||
|
val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber)
|
||||||
|
call?.mxCall?.transfer(result.userId, result.roomId)
|
||||||
|
_viewEvents.post(CallTransferViewEvents.Dismiss)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
_viewEvents.post(CallTransferViewEvents.FailToTransfer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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.app.features.call.transfer
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
|
||||||
|
data class CallTransferViewState(
|
||||||
|
val callId: String
|
||||||
|
) : MvRxState {
|
||||||
|
|
||||||
|
constructor(args: CallTransferArgs) : this(callId = args.callId)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.matrix.android.sdk.api.session.call
|
package im.vector.app.features.call.utils
|
||||||
|
|
||||||
import org.webrtc.EglBase
|
import org.webrtc.EglBase
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* 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.app.features.call.utils
|
||||||
|
|
||||||
|
import im.vector.app.features.call.webrtc.SdpObserverAdapter
|
||||||
|
import org.webrtc.MediaConstraints
|
||||||
|
import org.webrtc.PeerConnection
|
||||||
|
import org.webrtc.SessionDescription
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
suspend fun PeerConnection.awaitCreateOffer(mediaConstraints: MediaConstraints): SessionDescription? = suspendCoroutine { cont ->
|
||||||
|
createOffer(object : SdpObserverAdapter() {
|
||||||
|
override fun onCreateSuccess(p0: SessionDescription?) {
|
||||||
|
super.onCreateSuccess(p0)
|
||||||
|
cont.resume(p0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateFailure(p0: String?) {
|
||||||
|
super.onCreateFailure(p0)
|
||||||
|
cont.resumeWithException(IllegalStateException(p0))
|
||||||
|
}
|
||||||
|
}, mediaConstraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun PeerConnection.awaitCreateAnswer(mediaConstraints: MediaConstraints): SessionDescription? = suspendCoroutine { cont ->
|
||||||
|
createAnswer(object : SdpObserverAdapter() {
|
||||||
|
override fun onCreateSuccess(p0: SessionDescription?) {
|
||||||
|
super.onCreateSuccess(p0)
|
||||||
|
cont.resume(p0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateFailure(p0: String?) {
|
||||||
|
super.onCreateFailure(p0)
|
||||||
|
cont.resumeWithException(IllegalStateException(p0))
|
||||||
|
}
|
||||||
|
}, mediaConstraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun PeerConnection.awaitSetLocalDescription(sessionDescription: SessionDescription): Unit = suspendCoroutine { cont ->
|
||||||
|
setLocalDescription(object : SdpObserverAdapter() {
|
||||||
|
override fun onSetFailure(p0: String?) {
|
||||||
|
super.onSetFailure(p0)
|
||||||
|
cont.resumeWithException(IllegalStateException(p0))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSetSuccess() {
|
||||||
|
super.onSetSuccess()
|
||||||
|
cont.resume(Unit)
|
||||||
|
}
|
||||||
|
}, sessionDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun PeerConnection.awaitSetRemoteDescription(sessionDescription: SessionDescription): Unit = suspendCoroutine { cont ->
|
||||||
|
setRemoteDescription(object : SdpObserverAdapter() {
|
||||||
|
override fun onSetFailure(p0: String?) {
|
||||||
|
super.onSetFailure(p0)
|
||||||
|
cont.resumeWithException(IllegalStateException(p0))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSetSuccess() {
|
||||||
|
super.onSetSuccess()
|
||||||
|
cont.resume(Unit)
|
||||||
|
}
|
||||||
|
}, sessionDescription)
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* 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.app.features.call.utils
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallCandidate
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.SdpType
|
||||||
|
import org.webrtc.IceCandidate
|
||||||
|
import org.webrtc.SessionDescription
|
||||||
|
|
||||||
|
fun List<IceCandidate>.mapToCallCandidate() = map {
|
||||||
|
CallCandidate(
|
||||||
|
sdpMid = it.sdpMid,
|
||||||
|
sdpMLineIndex = it.sdpMLineIndex,
|
||||||
|
candidate = it.sdp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SdpType.asWebRTC(): SessionDescription.Type {
|
||||||
|
return if (this == SdpType.OFFER) {
|
||||||
|
SessionDescription.Type.OFFER
|
||||||
|
} else {
|
||||||
|
SessionDescription.Type.ANSWER
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.call.webrtc
|
||||||
|
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||||
|
|
||||||
|
private const val PSTN_VECTOR_KEY = "im.vector.protocol.pstn"
|
||||||
|
private const val PSTN_MATRIX_KEY = "m.protocol.pstn"
|
||||||
|
|
||||||
|
suspend fun Session.getSupportedPSTN(maxTries: Int): String? {
|
||||||
|
val thirdPartyProtocols: Map<String, ThirdPartyProtocol> = try {
|
||||||
|
thirdPartyService().getThirdPartyProtocols()
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
if (maxTries == 1) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
// Wait for 10s before trying again
|
||||||
|
delay(10_000L)
|
||||||
|
return getSupportedPSTN(maxTries - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return when {
|
||||||
|
thirdPartyProtocols.containsKey(PSTN_VECTOR_KEY) -> PSTN_VECTOR_KEY
|
||||||
|
thirdPartyProtocols.containsKey(PSTN_MATRIX_KEY) -> PSTN_MATRIX_KEY
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue