mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-24 22:17:25 +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 'org.conscrypt:conscrypt-android:2.0.0'
|
||||
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 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
|
||||
@@ -153,7 +154,7 @@ dependencies {
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 220
|
||||
def canonicalVersionCode = 222
|
||||
def canonicalVersionName = "1.11.9"
|
||||
|
||||
def postFixSize = 10
|
||||
@@ -232,6 +233,7 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
debuggable true
|
||||
minifyEnabled false
|
||||
}
|
||||
debug {
|
||||
|
@@ -297,6 +297,7 @@
|
||||
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
|
||||
<activity android:name="org.thoughtcrime.securesms.calls.WebRtcTestsActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:launchMode="singleTop"
|
||||
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar">
|
||||
<meta-data
|
||||
|
@@ -1,85 +1,136 @@
|
||||
package org.thoughtcrime.securesms.calls
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import kotlinx.android.synthetic.main.activity_webrtc_tests.*
|
||||
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.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.webrtc.*
|
||||
import org.webrtc.RendererCommon.ScalingType
|
||||
|
||||
|
||||
class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection.Observer,
|
||||
SdpObserver, RendererCommon.RendererEvents {
|
||||
SdpObserver {
|
||||
|
||||
companion object {
|
||||
const val HD_VIDEO_WIDTH = 1280
|
||||
const val HD_VIDEO_HEIGHT = 720
|
||||
const val HD_VIDEO_WIDTH = 320
|
||||
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 var target: VideoSink? = null
|
||||
private val eglBase by lazy { EglBase.create() }
|
||||
private val surfaceHelper by lazy { SurfaceTextureHelper.create(Thread.currentThread().name, eglBase.eglBaseContext) }
|
||||
|
||||
@Synchronized
|
||||
override fun onFrame(frame: VideoFrame) {
|
||||
if (target == null) {
|
||||
Log.d("Loki-RTC", "Dropping frame in proxy because target is null.")
|
||||
return
|
||||
}
|
||||
target!!.onFrame(frame)
|
||||
}
|
||||
private val connectionFactory by lazy {
|
||||
|
||||
@Synchronized
|
||||
fun setTarget(target: VideoSink?) {
|
||||
this.target = target
|
||||
}
|
||||
val decoderFactory = DefaultVideoDecoderFactory(eglBase.eglBaseContext)
|
||||
val encoderFactory = DefaultVideoEncoderFactory(eglBase.eglBaseContext, true, true)
|
||||
|
||||
PeerConnectionFactory.builder()
|
||||
.setVideoDecoderFactory(decoderFactory)
|
||||
.setVideoEncoderFactory(encoderFactory)
|
||||
.setOptions(PeerConnectionFactory.Options())
|
||||
.createPeerConnectionFactory()
|
||||
}
|
||||
|
||||
private val connectionFactory by lazy { PeerConnectionFactory.builder().createPeerConnectionFactory() }
|
||||
private val remoteVideoSink = ProxyVideoSink()
|
||||
private val localVideoSink = ProxyVideoSink()
|
||||
private val candidates: MutableList<IceCandidate> = mutableListOf()
|
||||
private val iceDebouncer = Debouncer(2_000)
|
||||
|
||||
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) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
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 audioTrack = connectionFactory.createAudioTrack("audio", audioSource)
|
||||
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 surfaceHelper = SurfaceTextureHelper.create("video-thread", eglBase.eglBaseContext)
|
||||
surfaceHelper.startListening(localVideoSink)
|
||||
videoCapturer.initialize(surfaceHelper, this, null)
|
||||
videoCapturer.initialize(surfaceHelper, local_renderer.context, videoSource.capturerObserver)
|
||||
videoCapturer.startCapture(HD_VIDEO_WIDTH, HD_VIDEO_HEIGHT, 10)
|
||||
|
||||
videoCapturer.startCapture(HD_VIDEO_WIDTH, HD_VIDEO_HEIGHT, 30)
|
||||
peerConnection.createOffer(this, MediaConstraints())
|
||||
val audioTrack = connectionFactory.createAudioTrack(LOCAL_TRACK_ID + "_audio", audioSource)
|
||||
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? {
|
||||
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
|
||||
Log.d("Loki-RTC-vid", "Looking for other cameras.")
|
||||
for (deviceName in deviceNames) {
|
||||
@@ -113,14 +162,6 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection
|
||||
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?) {
|
||||
Log.d("Loki-RTC", "onSignalingChange: $p0")
|
||||
}
|
||||
@@ -135,18 +176,48 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection
|
||||
|
||||
override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) {
|
||||
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?) {
|
||||
Log.d("Loki-RTC", "onIceCandidate: $p0")
|
||||
override fun onIceCandidate(iceCandidate: IceCandidate?) {
|
||||
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>?) {
|
||||
Log.d("Loki-RTC", "onIceCandidatesRemoved: $p0")
|
||||
peerConnection.removeIceCandidates(p0)
|
||||
}
|
||||
|
||||
override fun onAddStream(p0: MediaStream?) {
|
||||
Log.d("Loki-RTC", "onAddStream: $p0")
|
||||
override fun onAddStream(remoteStream: MediaStream?) {
|
||||
Log.d("Loki-RTC", "onAddStream: $remoteStream")
|
||||
if (remoteStream == null) {
|
||||
return
|
||||
}
|
||||
|
||||
remoteStream.videoTracks.firstOrNull()?.addSink(remote_renderer)
|
||||
}
|
||||
|
||||
override fun onRemoveStream(p0: MediaStream?) {
|
||||
@@ -165,8 +236,25 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection
|
||||
Log.d("Loki-RTC", "onAddTrack: $p0: $p1")
|
||||
}
|
||||
|
||||
override fun onCreateSuccess(p0: SessionDescription) {
|
||||
Log.d("Loki-RTC", "onCreateSuccess: ${p0.description}, ${p0.type}")
|
||||
override fun onCreateSuccess(sdp: SessionDescription) {
|
||||
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() {
|
||||
|
@@ -36,6 +36,7 @@ import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.*
|
||||
import org.thoughtcrime.securesms.calls.WebRtcTestsActivity
|
||||
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
|
||||
@@ -98,6 +99,11 @@ object ConversationMenuHelper {
|
||||
inflater.inflate(R.menu.menu_conversation_notification_settings, menu)
|
||||
}
|
||||
|
||||
// Call Tests
|
||||
if (!isOpenGroup) {
|
||||
inflater.inflate(R.menu.menu_conversation_call, menu)
|
||||
}
|
||||
|
||||
// Search
|
||||
val searchViewItem = menu.findItem(R.id.menu_search)
|
||||
(context as ConversationActivityV2).searchViewItem = searchViewItem
|
||||
@@ -158,6 +164,7 @@ object ConversationMenuHelper {
|
||||
R.id.menu_unmute_notifications -> { unmute(context, thread) }
|
||||
R.id.menu_mute_notifications -> { mute(context, thread) }
|
||||
R.id.menu_notification_settings -> { setNotifyType(context, thread) }
|
||||
R.id.menu_call -> { call(context, thread) }
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -174,6 +181,13 @@ object ConversationMenuHelper {
|
||||
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")
|
||||
private fun addShortcut(context: Context, thread: Recipient) {
|
||||
object : AsyncTask<Void?, Void?, IconCompat?>() {
|
||||
|
@@ -12,6 +12,7 @@ import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -32,8 +33,10 @@ import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
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.Util
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
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.onboarding.SeedActivity
|
||||
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
|
||||
import org.thoughtcrime.securesms.preferences.SettingsActivity
|
||||
import org.thoughtcrime.securesms.util.*
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
@@ -131,6 +135,36 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
}
|
||||
this.broadcastReceiver = broadcastReceiver
|
||||
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 {
|
||||
launch(Dispatchers.IO) {
|
||||
// Double check that the long poller is up
|
||||
@@ -400,8 +434,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
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_resend_message">Resend 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 -->
|
||||
<string name="conversation_context_image__save_attachment">Save attachment</string>
|
||||
|
Reference in New Issue
Block a user