mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-25 17:37:57 +00:00
feat: added basic call functionality
This commit is contained in:
@@ -53,7 +53,8 @@ dependencies {
|
|||||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||||
implementation 'org.whispersystems:webrtc-android:M77'
|
// implementation 'org.whispersystems:webrtc-android:M77'
|
||||||
|
implementation 'org.webrtc:google-webrtc:1.0.32006'
|
||||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||||
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
|
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
|
||||||
@@ -153,7 +154,7 @@ dependencies {
|
|||||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 220
|
def canonicalVersionCode = 222
|
||||||
def canonicalVersionName = "1.11.9"
|
def canonicalVersionName = "1.11.9"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
@@ -232,6 +233,7 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
|
debuggable true
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
|
@@ -297,6 +297,7 @@
|
|||||||
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
|
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
|
||||||
<activity android:name="org.thoughtcrime.securesms.calls.WebRtcTestsActivity"
|
<activity android:name="org.thoughtcrime.securesms.calls.WebRtcTestsActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
|
android:launchMode="singleTop"
|
||||||
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
|
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar">
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar">
|
||||||
<meta-data
|
<meta-data
|
||||||
|
@@ -1,85 +1,136 @@
|
|||||||
package org.thoughtcrime.securesms.calls
|
package org.thoughtcrime.securesms.calls
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import kotlinx.android.synthetic.main.activity_webrtc_tests.*
|
import kotlinx.android.synthetic.main.activity_webrtc_tests.*
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.messaging.messages.control.CallMessage
|
||||||
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.Debouncer
|
||||||
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.webrtc.*
|
import org.webrtc.*
|
||||||
import org.webrtc.RendererCommon.ScalingType
|
|
||||||
|
|
||||||
|
|
||||||
class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection.Observer,
|
class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection.Observer,
|
||||||
SdpObserver, RendererCommon.RendererEvents {
|
SdpObserver {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val HD_VIDEO_WIDTH = 1280
|
const val HD_VIDEO_WIDTH = 320
|
||||||
const val HD_VIDEO_HEIGHT = 720
|
const val HD_VIDEO_HEIGHT = 240
|
||||||
|
const val CALL_ID = "call_id_session"
|
||||||
|
private const val LOCAL_TRACK_ID = "local_track"
|
||||||
|
private const val LOCAL_STREAM_ID = "local_track"
|
||||||
|
|
||||||
|
const val ACTION_ANSWER = "answer"
|
||||||
|
const val ACTION_UPDATE_ICE = "updateIce"
|
||||||
|
|
||||||
|
const val EXTRA_SDP = "WebRtcTestsActivity_EXTRA_SDP"
|
||||||
|
const val EXTRA_ADDRESS = "WebRtcTestsActivity_EXTRA_ADDRESS"
|
||||||
|
const val EXTRA_SDP_MLINE_INDEXES = "WebRtcTestsActivity_EXTRA_SDP_MLINE_INDEXES"
|
||||||
|
const val EXTRA_SDP_MIDS = "WebRtcTestsActivity_EXTRA_SDP_MIDS"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ProxyVideoSink : VideoSink {
|
private val eglBase by lazy { EglBase.create() }
|
||||||
private var target: VideoSink? = null
|
private val surfaceHelper by lazy { SurfaceTextureHelper.create(Thread.currentThread().name, eglBase.eglBaseContext) }
|
||||||
|
|
||||||
@Synchronized
|
private val connectionFactory by lazy {
|
||||||
override fun onFrame(frame: VideoFrame) {
|
|
||||||
if (target == null) {
|
|
||||||
Log.d("Loki-RTC", "Dropping frame in proxy because target is null.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
target!!.onFrame(frame)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
val decoderFactory = DefaultVideoDecoderFactory(eglBase.eglBaseContext)
|
||||||
fun setTarget(target: VideoSink?) {
|
val encoderFactory = DefaultVideoEncoderFactory(eglBase.eglBaseContext, true, true)
|
||||||
this.target = target
|
|
||||||
}
|
PeerConnectionFactory.builder()
|
||||||
|
.setVideoDecoderFactory(decoderFactory)
|
||||||
|
.setVideoEncoderFactory(encoderFactory)
|
||||||
|
.setOptions(PeerConnectionFactory.Options())
|
||||||
|
.createPeerConnectionFactory()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val connectionFactory by lazy { PeerConnectionFactory.builder().createPeerConnectionFactory() }
|
private val candidates: MutableList<IceCandidate> = mutableListOf()
|
||||||
private val remoteVideoSink = ProxyVideoSink()
|
private val iceDebouncer = Debouncer(2_000)
|
||||||
private val localVideoSink = ProxyVideoSink()
|
|
||||||
|
private lateinit var callAddress: Address
|
||||||
|
private val peerConnection by lazy {
|
||||||
|
// TODO: in a lokinet world, ice servers shouldn't be needed as .loki addresses should suffice to p2p
|
||||||
|
val server = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer()
|
||||||
|
val server1 = PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer()
|
||||||
|
val server2 = PeerConnection.IceServer.builder("stun:stun2.l.google.com:19302").createIceServer()
|
||||||
|
val server3 = PeerConnection.IceServer.builder("stun:stun3.l.google.com:19302").createIceServer()
|
||||||
|
val server4 = PeerConnection.IceServer.builder("stun:stun4.l.google.com:19302").createIceServer()
|
||||||
|
val rtcConfig = PeerConnection.RTCConfiguration(listOf(server, server1, server2, server3, server4))
|
||||||
|
rtcConfig.keyType = PeerConnection.KeyType.ECDSA
|
||||||
|
connectionFactory.createPeerConnection(rtcConfig, this)!!
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||||
super.onCreate(savedInstanceState, ready)
|
super.onCreate(savedInstanceState, ready)
|
||||||
setContentView(R.layout.activity_webrtc_tests)
|
setContentView(R.layout.activity_webrtc_tests)
|
||||||
|
|
||||||
val server = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer()
|
local_renderer.run {
|
||||||
|
setMirror(true)
|
||||||
|
setEnableHardwareScaler(true)
|
||||||
|
init(eglBase.eglBaseContext, null)
|
||||||
|
}
|
||||||
|
remote_renderer.run {
|
||||||
|
setMirror(true)
|
||||||
|
setEnableHardwareScaler(true)
|
||||||
|
init(eglBase.eglBaseContext, null)
|
||||||
|
}
|
||||||
|
|
||||||
val rtcConfig = PeerConnection.RTCConfiguration(listOf(server))
|
|
||||||
rtcConfig.keyType = PeerConnection.KeyType.ECDSA
|
|
||||||
rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
|
|
||||||
|
|
||||||
val peerConnection = connectionFactory.createPeerConnection(rtcConfig, this) ?: return
|
|
||||||
|
|
||||||
Log.d("Loki-RTC", "peer connecting?")
|
|
||||||
val stream = connectionFactory.createLocalMediaStream("stream")
|
|
||||||
val audioSource = connectionFactory.createAudioSource(MediaConstraints())
|
val audioSource = connectionFactory.createAudioSource(MediaConstraints())
|
||||||
val audioTrack = connectionFactory.createAudioTrack("audio", audioSource)
|
|
||||||
val videoSource = connectionFactory.createVideoSource(false)
|
val videoSource = connectionFactory.createVideoSource(false)
|
||||||
val videoTrack = connectionFactory.createVideoTrack("video", videoSource)
|
|
||||||
stream.addTrack(audioTrack)
|
|
||||||
stream.addTrack(videoTrack)
|
|
||||||
val remoteTrack = getRemoteVideoTrack(peerConnection) ?: return
|
|
||||||
videoTrack.addSink(localVideoSink)
|
|
||||||
remoteTrack.addSink(remoteVideoSink)
|
|
||||||
remoteTrack.setEnabled(true)
|
|
||||||
videoTrack.setEnabled(true)
|
|
||||||
|
|
||||||
val eglBase = EglBase.create()
|
|
||||||
local_renderer.init(eglBase.eglBaseContext, this)
|
|
||||||
local_renderer.setScalingType(ScalingType.SCALE_ASPECT_FILL)
|
|
||||||
remote_renderer.init(eglBase.eglBaseContext, this)
|
|
||||||
|
|
||||||
val videoCapturer = createCameraCapturer(Camera2Enumerator(this)) ?: kotlin.run { finish(); return }
|
val videoCapturer = createCameraCapturer(Camera2Enumerator(this)) ?: kotlin.run { finish(); return }
|
||||||
val surfaceHelper = SurfaceTextureHelper.create("video-thread", eglBase.eglBaseContext)
|
videoCapturer.initialize(surfaceHelper, local_renderer.context, videoSource.capturerObserver)
|
||||||
surfaceHelper.startListening(localVideoSink)
|
videoCapturer.startCapture(HD_VIDEO_WIDTH, HD_VIDEO_HEIGHT, 10)
|
||||||
videoCapturer.initialize(surfaceHelper, this, null)
|
|
||||||
|
|
||||||
videoCapturer.startCapture(HD_VIDEO_WIDTH, HD_VIDEO_HEIGHT, 30)
|
val audioTrack = connectionFactory.createAudioTrack(LOCAL_TRACK_ID + "_audio", audioSource)
|
||||||
peerConnection.createOffer(this, MediaConstraints())
|
val videoTrack = connectionFactory.createVideoTrack(LOCAL_TRACK_ID, videoSource)
|
||||||
|
videoTrack.addSink(local_renderer)
|
||||||
|
|
||||||
|
val stream = connectionFactory.createLocalMediaStream(LOCAL_STREAM_ID)
|
||||||
|
stream.addTrack(videoTrack)
|
||||||
|
stream.addTrack(audioTrack)
|
||||||
|
|
||||||
|
peerConnection.addStream(stream)
|
||||||
|
|
||||||
|
// create either call or answer
|
||||||
|
if (intent.action == ACTION_ANSWER) {
|
||||||
|
callAddress = intent.getParcelableExtra(EXTRA_ADDRESS) ?: run { finish(); return }
|
||||||
|
val offerSdp = intent.getStringArrayExtra(EXTRA_SDP)!![0]
|
||||||
|
peerConnection.setRemoteDescription(this, SessionDescription(SessionDescription.Type.OFFER, offerSdp))
|
||||||
|
peerConnection.createAnswer(this, MediaConstraints())
|
||||||
|
} else {
|
||||||
|
callAddress = intent.getParcelableExtra(EXTRA_ADDRESS) ?: run { finish(); return }
|
||||||
|
peerConnection.createOffer(this, MediaConstraints())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRemoteVideoTrack(peerConnection: PeerConnection): VideoTrack? = peerConnection.transceivers.firstOrNull { it.receiver.track() is VideoTrack } as VideoTrack?
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
if (intent == null) return
|
||||||
|
callAddress = intent.getParcelableExtra(EXTRA_ADDRESS) ?: run { finish(); return }
|
||||||
|
when (intent.action) {
|
||||||
|
ACTION_ANSWER -> {
|
||||||
|
peerConnection.setRemoteDescription(this,
|
||||||
|
SessionDescription(SessionDescription.Type.ANSWER, intent.getStringArrayExtra(EXTRA_SDP)!![0])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ACTION_UPDATE_ICE -> {
|
||||||
|
val sdpIndexes = intent.getIntArrayExtra(EXTRA_SDP_MLINE_INDEXES)!!
|
||||||
|
val sdpMids = intent.getStringArrayExtra(EXTRA_SDP_MIDS)!!
|
||||||
|
val sdp = intent.getStringArrayExtra(EXTRA_SDP)!!
|
||||||
|
val amount = minOf(sdpIndexes.size, sdpMids.size)
|
||||||
|
(0 until amount).map { index ->
|
||||||
|
val candidate = IceCandidate(sdpMids[index], sdpIndexes[index], sdp[index])
|
||||||
|
peerConnection.addIceCandidate(candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createCameraCapturer(enumerator: CameraEnumerator): VideoCapturer? {
|
private fun createCameraCapturer(enumerator: CameraEnumerator): VideoCapturer? {
|
||||||
val deviceNames = enumerator.deviceNames
|
val deviceNames = enumerator.deviceNames
|
||||||
@@ -96,8 +147,6 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Front facing camera not found, try something else
|
|
||||||
|
|
||||||
// Front facing camera not found, try something else
|
// Front facing camera not found, try something else
|
||||||
Log.d("Loki-RTC-vid", "Looking for other cameras.")
|
Log.d("Loki-RTC-vid", "Looking for other cameras.")
|
||||||
for (deviceName in deviceNames) {
|
for (deviceName in deviceNames) {
|
||||||
@@ -113,14 +162,6 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFirstFrameRendered() {
|
|
||||||
Log.d("Loki-RTC-vid", "first frame rendered")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFrameResolutionChanged(p0: Int, p1: Int, p2: Int) {
|
|
||||||
Log.d("Loki-RTC-vid", "frame resolution changed")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
|
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
|
||||||
Log.d("Loki-RTC", "onSignalingChange: $p0")
|
Log.d("Loki-RTC", "onSignalingChange: $p0")
|
||||||
}
|
}
|
||||||
@@ -135,18 +176,48 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection
|
|||||||
|
|
||||||
override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) {
|
override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) {
|
||||||
Log.d("Loki-RTC", "onIceGatheringChange: $p0")
|
Log.d("Loki-RTC", "onIceGatheringChange: $p0")
|
||||||
|
p0 ?: return
|
||||||
|
Log.d("Loki-RTC","sending IceCandidates of size: ${candidates.size}")
|
||||||
|
MessageSender.sendNonDurably(
|
||||||
|
CallMessage(SignalServiceProtos.CallMessage.Type.ICE_CANDIDATES,
|
||||||
|
candidates.map { it.sdp },
|
||||||
|
candidates.map { it.sdpMLineIndex },
|
||||||
|
candidates.map { it.sdpMid }
|
||||||
|
),
|
||||||
|
callAddress
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIceCandidate(p0: IceCandidate?) {
|
override fun onIceCandidate(iceCandidate: IceCandidate?) {
|
||||||
Log.d("Loki-RTC", "onIceCandidate: $p0")
|
Log.d("Loki-RTC", "onIceCandidate: $iceCandidate")
|
||||||
|
if (iceCandidate == null) return
|
||||||
|
// TODO: in a lokinet world, these might have to be filtered specifically to drop anything that is not .loki
|
||||||
|
peerConnection.addIceCandidate(iceCandidate)
|
||||||
|
candidates.add(iceCandidate)
|
||||||
|
iceDebouncer.publish {
|
||||||
|
MessageSender.sendNonDurably(
|
||||||
|
CallMessage(SignalServiceProtos.CallMessage.Type.ICE_CANDIDATES,
|
||||||
|
candidates.map { it.sdp },
|
||||||
|
candidates.map { it.sdpMLineIndex },
|
||||||
|
candidates.map { it.sdpMid }
|
||||||
|
),
|
||||||
|
callAddress
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIceCandidatesRemoved(p0: Array<out IceCandidate>?) {
|
override fun onIceCandidatesRemoved(p0: Array<out IceCandidate>?) {
|
||||||
Log.d("Loki-RTC", "onIceCandidatesRemoved: $p0")
|
Log.d("Loki-RTC", "onIceCandidatesRemoved: $p0")
|
||||||
|
peerConnection.removeIceCandidates(p0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAddStream(p0: MediaStream?) {
|
override fun onAddStream(remoteStream: MediaStream?) {
|
||||||
Log.d("Loki-RTC", "onAddStream: $p0")
|
Log.d("Loki-RTC", "onAddStream: $remoteStream")
|
||||||
|
if (remoteStream == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteStream.videoTracks.firstOrNull()?.addSink(remote_renderer)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRemoveStream(p0: MediaStream?) {
|
override fun onRemoveStream(p0: MediaStream?) {
|
||||||
@@ -165,8 +236,25 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection
|
|||||||
Log.d("Loki-RTC", "onAddTrack: $p0: $p1")
|
Log.d("Loki-RTC", "onAddTrack: $p0: $p1")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateSuccess(p0: SessionDescription) {
|
override fun onCreateSuccess(sdp: SessionDescription) {
|
||||||
Log.d("Loki-RTC", "onCreateSuccess: ${p0.description}, ${p0.type}")
|
Log.d("Loki-RTC", "onCreateSuccess: ${sdp.type}")
|
||||||
|
when (sdp.type) {
|
||||||
|
SessionDescription.Type.OFFER -> {
|
||||||
|
peerConnection.setLocalDescription(this, sdp)
|
||||||
|
MessageSender.sendNonDurably(
|
||||||
|
CallMessage(SignalServiceProtos.CallMessage.Type.OFFER, listOf(sdp.description), listOf(), listOf()),
|
||||||
|
callAddress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SessionDescription.Type.ANSWER -> {
|
||||||
|
peerConnection.setLocalDescription(this, sdp)
|
||||||
|
MessageSender.sendNonDurably(
|
||||||
|
CallMessage(SignalServiceProtos.CallMessage.Type.ANSWER, listOf(sdp.description), listOf(), listOf()),
|
||||||
|
callAddress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SessionDescription.Type.PRANSWER -> TODO("do the PR answer create success handling") // MessageSender.send()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSetSuccess() {
|
override fun onSetSuccess() {
|
||||||
|
@@ -36,6 +36,7 @@ import org.session.libsession.utilities.recipients.Recipient
|
|||||||
import org.session.libsignal.utilities.guava.Optional
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
import org.thoughtcrime.securesms.*
|
import org.thoughtcrime.securesms.*
|
||||||
|
import org.thoughtcrime.securesms.calls.WebRtcTestsActivity
|
||||||
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
|
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
|
import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
|
||||||
@@ -98,6 +99,11 @@ object ConversationMenuHelper {
|
|||||||
inflater.inflate(R.menu.menu_conversation_notification_settings, menu)
|
inflater.inflate(R.menu.menu_conversation_notification_settings, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call Tests
|
||||||
|
if (!isOpenGroup) {
|
||||||
|
inflater.inflate(R.menu.menu_conversation_call, menu)
|
||||||
|
}
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
val searchViewItem = menu.findItem(R.id.menu_search)
|
val searchViewItem = menu.findItem(R.id.menu_search)
|
||||||
(context as ConversationActivityV2).searchViewItem = searchViewItem
|
(context as ConversationActivityV2).searchViewItem = searchViewItem
|
||||||
@@ -158,6 +164,7 @@ object ConversationMenuHelper {
|
|||||||
R.id.menu_unmute_notifications -> { unmute(context, thread) }
|
R.id.menu_unmute_notifications -> { unmute(context, thread) }
|
||||||
R.id.menu_mute_notifications -> { mute(context, thread) }
|
R.id.menu_mute_notifications -> { mute(context, thread) }
|
||||||
R.id.menu_notification_settings -> { setNotifyType(context, thread) }
|
R.id.menu_notification_settings -> { setNotifyType(context, thread) }
|
||||||
|
R.id.menu_call -> { call(context, thread) }
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -174,6 +181,13 @@ object ConversationMenuHelper {
|
|||||||
searchViewModel.onSearchOpened()
|
searchViewModel.onSearchOpened()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun call(context: Context, thread: Recipient) {
|
||||||
|
val intent = Intent(context, WebRtcTestsActivity::class.java)
|
||||||
|
intent.putExtra(WebRtcTestsActivity.EXTRA_ADDRESS, thread.address)
|
||||||
|
val activity = context as AppCompatActivity
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private fun addShortcut(context: Context, thread: Recipient) {
|
private fun addShortcut(context: Context, thread: Recipient) {
|
||||||
object : AsyncTask<Void?, Void?, IconCompat?>() {
|
object : AsyncTask<Void?, Void?, IconCompat?>() {
|
||||||
|
@@ -12,6 +12,7 @@ import android.text.SpannableString
|
|||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -32,8 +33,10 @@ import org.greenrobot.eventbus.Subscribe
|
|||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
|
import org.session.libsession.messaging.utilities.WebRtcUtils
|
||||||
import org.session.libsession.utilities.*
|
import org.session.libsession.utilities.*
|
||||||
import org.session.libsession.utilities.Util
|
import org.session.libsession.utilities.Util
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
@@ -53,6 +56,7 @@ import org.thoughtcrime.securesms.mms.GlideApp
|
|||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.onboarding.SeedActivity
|
import org.thoughtcrime.securesms.onboarding.SeedActivity
|
||||||
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
|
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
|
||||||
|
import org.thoughtcrime.securesms.preferences.SettingsActivity
|
||||||
import org.thoughtcrime.securesms.util.*
|
import org.thoughtcrime.securesms.util.*
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -131,6 +135,36 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
|||||||
}
|
}
|
||||||
this.broadcastReceiver = broadcastReceiver
|
this.broadcastReceiver = broadcastReceiver
|
||||||
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged"))
|
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged"))
|
||||||
|
lifecycleScope.launchWhenCreated {
|
||||||
|
// web rtc channel handling
|
||||||
|
for (message in WebRtcUtils.SIGNAL_QUEUE) {
|
||||||
|
val sender = Address.fromSerialized(message.sender!!)
|
||||||
|
val intent = Intent(this@HomeActivity, WebRtcTestsActivity::class.java)
|
||||||
|
val bundle = bundleOf(
|
||||||
|
WebRtcTestsActivity.EXTRA_ADDRESS to sender,
|
||||||
|
)
|
||||||
|
if (message.sdps.isNotEmpty() && message.sdpMids.isEmpty()) {
|
||||||
|
// offer message
|
||||||
|
Log.d("Loki-RTC", "answer receive")
|
||||||
|
val sdps = message.sdps
|
||||||
|
intent.action = WebRtcTestsActivity.ACTION_ANSWER
|
||||||
|
bundle.putStringArray(WebRtcTestsActivity.EXTRA_SDP, sdps.toTypedArray())
|
||||||
|
} else if (message.sdpMids.isNotEmpty()) {
|
||||||
|
// ice candidates message
|
||||||
|
Log.d("Loki-RTC", "update ice intent")
|
||||||
|
val sdpMLineIndexes = message.sdpMLineIndexes
|
||||||
|
val sdpMids = message.sdpMids
|
||||||
|
val sdps = message.sdps
|
||||||
|
intent.action = WebRtcTestsActivity.ACTION_UPDATE_ICE
|
||||||
|
bundle.putStringArray(WebRtcTestsActivity.EXTRA_SDP, sdps.toTypedArray())
|
||||||
|
bundle.putIntArray(WebRtcTestsActivity.EXTRA_SDP_MLINE_INDEXES, sdpMLineIndexes.toIntArray())
|
||||||
|
bundle.putStringArray(WebRtcTestsActivity.EXTRA_SDP_MIDS, sdpMids.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
intent.putExtras(bundle)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
lifecycleScope.launchWhenStarted {
|
lifecycleScope.launchWhenStarted {
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
// Double check that the long poller is up
|
// Double check that the long poller is up
|
||||||
@@ -400,8 +434,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun openSettings() {
|
private fun openSettings() {
|
||||||
val intent = Intent(this, WebRtcTestsActivity::class.java)
|
val intent = Intent(this, SettingsActivity::class.java)
|
||||||
// val intent = Intent(this, SettingsActivity::class.java)
|
|
||||||
show(intent, isForResult = false)
|
show(intent, isForResult = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
10
app/src/main/res/drawable/ic_baseline_call_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_call_24.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20.01,15.38c-1.23,0 -2.42,-0.2 -3.53,-0.56 -0.35,-0.12 -0.74,-0.03 -1.01,0.24l-1.57,1.97c-2.83,-1.35 -5.48,-3.9 -6.89,-6.83l1.95,-1.66c0.27,-0.28 0.35,-0.67 0.24,-1.02 -0.37,-1.11 -0.56,-2.3 -0.56,-3.53 0,-0.54 -0.45,-0.99 -0.99,-0.99H4.19C3.65,3 3,3.24 3,3.99 3,13.28 10.73,21 20.01,21c0.71,0 0.99,-0.63 0.99,-1.18v-3.45c0,-0.54 -0.45,-0.99 -0.99,-0.99z"/>
|
||||||
|
</vector>
|
9
app/src/main/res/menu/menu_conversation_call.xml
Normal file
9
app/src/main/res/menu/menu_conversation_call.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:title="@string/conversation_context__menu_call"
|
||||||
|
android:icon="@drawable/ic_baseline_call_24"
|
||||||
|
app:showAsAction="always"
|
||||||
|
android:id="@+id/menu_call"/>
|
||||||
|
</menu>
|
@@ -585,6 +585,7 @@
|
|||||||
<string name="conversation_context__menu_ban_and_delete_all">Ban and delete all</string>
|
<string name="conversation_context__menu_ban_and_delete_all">Ban and delete all</string>
|
||||||
<string name="conversation_context__menu_resend_message">Resend message</string>
|
<string name="conversation_context__menu_resend_message">Resend message</string>
|
||||||
<string name="conversation_context__menu_reply_to_message">Reply to message</string>
|
<string name="conversation_context__menu_reply_to_message">Reply to message</string>
|
||||||
|
<string name="conversation_context__menu_call">Call</string>
|
||||||
|
|
||||||
<!-- conversation_context_image -->
|
<!-- conversation_context_image -->
|
||||||
<string name="conversation_context_image__save_attachment">Save attachment</string>
|
<string name="conversation_context_image__save_attachment">Save attachment</string>
|
||||||
|
@@ -0,0 +1,57 @@
|
|||||||
|
package org.session.libsession.messaging.messages.control
|
||||||
|
|
||||||
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
|
||||||
|
class CallMessage(): ControlMessage() {
|
||||||
|
var type: SignalServiceProtos.CallMessage.Type? = null
|
||||||
|
var sdps: List<String> = listOf()
|
||||||
|
var sdpMLineIndexes: List<Int> = listOf()
|
||||||
|
var sdpMids: List<String> = listOf()
|
||||||
|
|
||||||
|
override val isSelfSendValid: Boolean = false
|
||||||
|
|
||||||
|
override fun isValid(): Boolean = super.isValid() && type != null && !sdps.isNullOrEmpty()
|
||||||
|
|
||||||
|
constructor(type: SignalServiceProtos.CallMessage.Type,
|
||||||
|
sdps: List<String>,
|
||||||
|
sdpMLineIndexes: List<Int>,
|
||||||
|
sdpMids: List<String>) : this() {
|
||||||
|
this.type = type
|
||||||
|
this.sdps = sdps
|
||||||
|
this.sdpMLineIndexes = sdpMLineIndexes
|
||||||
|
this.sdpMids = sdpMids
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "CallMessage"
|
||||||
|
|
||||||
|
fun fromProto(proto: SignalServiceProtos.Content): CallMessage? {
|
||||||
|
val callMessageProto = if (proto.hasCallMessage()) proto.callMessage else return null
|
||||||
|
val type = callMessageProto.type
|
||||||
|
val sdps = callMessageProto.sdpsList
|
||||||
|
val sdpMLineIndexes = callMessageProto.sdpMLineIndexesList
|
||||||
|
val sdpMids = callMessageProto.sdpMidsList
|
||||||
|
return CallMessage(type,sdps, sdpMLineIndexes, sdpMids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toProto(): SignalServiceProtos.Content? {
|
||||||
|
val nonNullType = type ?: run {
|
||||||
|
Log.w(TAG,"Couldn't construct call message request proto from: $this")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val callMessage = SignalServiceProtos.CallMessage.newBuilder()
|
||||||
|
.setType(nonNullType)
|
||||||
|
.addAllSdps(sdps)
|
||||||
|
.addAllSdpMLineIndexes(sdpMLineIndexes)
|
||||||
|
.addAllSdpMids(sdpMids)
|
||||||
|
|
||||||
|
return SignalServiceProtos.Content.newBuilder()
|
||||||
|
.setCallMessage(
|
||||||
|
callMessage
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
@@ -95,6 +95,7 @@ object MessageReceiver {
|
|||||||
ExpirationTimerUpdate.fromProto(proto) ?:
|
ExpirationTimerUpdate.fromProto(proto) ?:
|
||||||
ConfigurationMessage.fromProto(proto) ?:
|
ConfigurationMessage.fromProto(proto) ?:
|
||||||
UnsendRequest.fromProto(proto) ?:
|
UnsendRequest.fromProto(proto) ?:
|
||||||
|
CallMessage.fromProto(proto) ?:
|
||||||
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
|
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
|
||||||
// Ignore self send if needed
|
// Ignore self send if needed
|
||||||
if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend
|
if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend
|
||||||
|
@@ -14,24 +14,20 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi
|
|||||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||||
|
import org.session.libsession.messaging.utilities.WebRtcUtils
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.*
|
||||||
import org.session.libsession.utilities.GroupRecord
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsession.utilities.GroupUtil
|
|
||||||
import org.session.libsession.utilities.SSKEnvironment
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsession.utilities.ProfileKeyUtil
|
|
||||||
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
||||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||||
import org.session.libsignal.utilities.guava.Optional
|
|
||||||
import org.session.libsignal.messages.SignalServiceGroup
|
import org.session.libsignal.messages.SignalServiceGroup
|
||||||
import org.session.libsignal.protos.SignalServiceProtos
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
|
||||||
import org.session.libsignal.utilities.toHexString
|
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
|
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||||
|
import org.session.libsignal.utilities.toHexString
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
@@ -52,6 +48,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
|
|||||||
is ConfigurationMessage -> handleConfigurationMessage(message)
|
is ConfigurationMessage -> handleConfigurationMessage(message)
|
||||||
is UnsendRequest -> handleUnsendRequest(message)
|
is UnsendRequest -> handleUnsendRequest(message)
|
||||||
is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID)
|
is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID)
|
||||||
|
is CallMessage -> handleCallMessage(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +58,11 @@ private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) {
|
|||||||
SSKEnvironment.shared.readReceiptManager.processReadReceipts(context, message.sender!!, message.timestamps!!, message.receivedTimestamp!!)
|
SSKEnvironment.shared.readReceiptManager.processReadReceipts(context, message.sender!!, message.timestamps!!, message.receivedTimestamp!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun MessageReceiver.handleCallMessage(message: CallMessage) {
|
||||||
|
// TODO: refactor this out to persistence, just to help debug the flow and send/receive in synchronous testing
|
||||||
|
WebRtcUtils.SIGNAL_QUEUE.offer(message)
|
||||||
|
}
|
||||||
|
|
||||||
private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) {
|
private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) {
|
||||||
when (message.kind!!) {
|
when (message.kind!!) {
|
||||||
TypingIndicator.Kind.STARTED -> showTypingIndicatorIfNeeded(message.sender!!)
|
TypingIndicator.Kind.STARTED -> showTypingIndicatorIfNeeded(message.sender!!)
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
package org.session.libsession.messaging.utilities
|
||||||
|
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import org.session.libsession.messaging.messages.control.CallMessage
|
||||||
|
|
||||||
|
object WebRtcUtils {
|
||||||
|
|
||||||
|
// TODO: move this to a better place that is persistent
|
||||||
|
val SIGNAL_QUEUE = Channel<CallMessage>(Channel.UNLIMITED)
|
||||||
|
|
||||||
|
}
|
@@ -168,12 +168,15 @@ message CallMessage {
|
|||||||
OFFER = 1;
|
OFFER = 1;
|
||||||
ANSWER = 2;
|
ANSWER = 2;
|
||||||
PROVISIONAL_ANSWER = 3;
|
PROVISIONAL_ANSWER = 3;
|
||||||
|
ICE_CANDIDATES = 4;
|
||||||
|
END_CALL = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @required
|
// @required
|
||||||
required Type type = 1;
|
required Type type = 1;
|
||||||
// @required
|
repeated string sdps = 2;
|
||||||
required string sdp = 2;
|
repeated uint32 sdpMLineIndexes = 3;
|
||||||
|
repeated string sdpMids = 4;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user