feat: adding more lifecycle vm and callmanager / call service functionality

This commit is contained in:
jubb
2021-11-04 12:07:06 +11:00
parent 2e3f46ff9f
commit 5cff5ffb45
11 changed files with 295 additions and 34 deletions

View File

@@ -297,7 +297,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="true"
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
<activity android:name="org.thoughtcrime.securesms.calls.WebRtcTestsActivity"
<activity android:name="org.thoughtcrime.securesms.calls.WebRtcCallActivity"
android:screenOrientation="portrait"
android:launchMode="singleTop"
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"

View File

@@ -11,7 +11,6 @@ import android.view.MenuItem
import android.view.Window
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
@@ -25,7 +24,6 @@ import org.session.libsession.messaging.messages.control.CallMessage
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.WebRtcUtils
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
@@ -35,7 +33,7 @@ import org.webrtc.*
import java.util.*
@AndroidEntryPoint
class WebRtcTestsActivity: PassphraseRequiredActionBarActivity() {
class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
companion object {
const val CALL_ID = "call_id_session"
@@ -48,6 +46,8 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity() {
const val EXTRA_SDP = "WebRtcTestsActivity_EXTRA_SDP"
const val EXTRA_ADDRESS = "WebRtcTestsActivity_EXTRA_ADDRESS"
const val EXTRA_CALL_ID = "WebRtcTestsActivity_EXTRA_CALL_ID"
const val BUSY_SIGNAL_DELAY_FINISH = 5500L
}
private val viewModel by viewModels<CallViewModel>()
@@ -135,7 +135,7 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity() {
}
if (answer != null) {
peerConnection.setRemoteDescription(
this@WebRtcTestsActivity,
this@WebRtcCallActivity,
SessionDescription(SessionDescription.Type.ANSWER, answer.sdps[0])
)
break
@@ -153,7 +153,7 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity() {
lifecycleScope.launchWhenResumed {
while (this.isActive) {
delay(2_000L)
peerConnection.getStats(this@WebRtcTestsActivity)
peerConnection.getStats(this@WebRtcCallActivity)
synchronized(WebRtcUtils.callCache) {
val set = WebRtcUtils.callCache[callId] ?: mutableSetOf()
set.filter { it.hashCode() !in acceptedCallMessageHashes

View File

@@ -36,7 +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.calls.WebRtcCallActivity
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
@@ -187,15 +187,15 @@ object ConversationMenuHelper {
.setTitle("Call")
.setMessage("Use relay?")
.setPositiveButton("Use Relay") { d, w ->
val intent = Intent(context, WebRtcTestsActivity::class.java)
intent.putExtra(WebRtcTestsActivity.EXTRA_CALL_ID, UUID.randomUUID().toString())
intent.putExtra(WebRtcTestsActivity.EXTRA_ADDRESS, thread.address)
val intent = Intent(context, WebRtcCallActivity::class.java)
intent.putExtra(WebRtcCallActivity.EXTRA_CALL_ID, UUID.randomUUID().toString())
intent.putExtra(WebRtcCallActivity.EXTRA_ADDRESS, thread.address)
val activity = context as AppCompatActivity
activity.startActivity(intent)
}
.setNeutralButton("P2P only") { d, w ->
val intent = Intent(context, WebRtcTestsActivity::class.java)
intent.putExtra(WebRtcTestsActivity.EXTRA_ADDRESS, thread.address)
val intent = Intent(context, WebRtcCallActivity::class.java)
intent.putExtra(WebRtcCallActivity.EXTRA_ADDRESS, thread.address)
val activity = context as AppCompatActivity
activity.startActivity(intent)
}

View File

@@ -45,7 +45,7 @@ import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.MuteDialog
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.calls.WebRtcTestsActivity
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
@@ -185,7 +185,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
synchronized(WebRtcUtils.callCache) {
WebRtcUtils.callCache[callId] = mutableSetOf()
}
sendBroadcast(Intent(WebRtcTestsActivity.ACTION_END))
sendBroadcast(Intent(WebRtcCallActivity.ACTION_END))
}
else -> { /* do nothing */ }
}

View File

@@ -6,18 +6,32 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import android.os.Build
import android.os.IBinder
import android.os.ResultReceiver
import android.telephony.PhoneStateListener
import android.telephony.TelephonyManager
import androidx.core.content.ContextCompat
import dagger.hilt.android.AndroidEntryPoint
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.FutureTaskListener
import org.session.libsession.utilities.Util
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
import org.thoughtcrime.securesms.util.CallNotificationBuilder
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_ESTABLISHED
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_CONNECTING
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_RINGING
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OUTGOING_RINGING
import org.thoughtcrime.securesms.webrtc.*
import org.thoughtcrime.securesms.webrtc.CallManager.CallState.*
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
import java.lang.AssertionError
import java.util.*
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@AndroidEntryPoint
@@ -26,6 +40,9 @@ class WebRtcCallService: Service() {
@Inject lateinit var callManager: CallManager
companion object {
private val TAG = Log.tag(WebRtcCallService::class.java)
const val ACTION_INCOMING_CALL = "CALL_INCOMING"
const val ACTION_OUTGOING_CALL = "CALL_OUTGOING"
const val ACTION_ANSWER_CALL = "ANSWER_CALL"
@@ -168,6 +185,100 @@ class WebRtcCallService: Service() {
registerReceiver(wiredHeadsetStateReceiver, IntentFilter(AudioManager.ACTION_HEADSET_PLUG))
}
private fun handleBusyCall(intent: Intent) {
val recipient = getRemoteRecipient(intent)
val callId = getCallId(intent)
val callState = callManager.currentConnectionState
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
when (callState) {
STATE_DIALING,
STATE_REMOTE_RINGING -> setCallInProgressNotification(TYPE_OUTGOING_RINGING, callManager.recipient)
STATE_IDLE -> setCallInProgressNotification(TYPE_INCOMING_CONNECTING, recipient)
STATE_ANSWERING -> setCallInProgressNotification(TYPE_INCOMING_CONNECTING, callManager.recipient)
STATE_LOCAL_RINGING -> setCallInProgressNotification(TYPE_INCOMING_RINGING, callManager.recipient)
STATE_CONNECTED -> setCallInProgressNotification(TYPE_ESTABLISHED, callManager.recipient)
else -> throw AssertionError()
}
}
if (callState == STATE_IDLE) {
stopForeground(true)
}
// TODO: send hangup via messageSender
insertMissedCall(getRemoteRecipient(intent), false)
}
private fun handleBusyMessage(intent: Intent) {
val recipient = getRemoteRecipient(intent)
val callId = getCallId(intent)
if (callManager.currentConnectionState != STATE_DIALING || callManager.callId != callManager.callId || callManager.recipient != callManager.recipient) {
Log.w(TAG,"Got busy message for inactive session...")
return
}
callManager.postViewModelState(CallViewModel.State.CALL_BUSY)
callManager.startOutgoingRinger(OutgoingRinger.Type.BUSY)
Util.runOnMainDelayed({
startService(
Intent(this, WebRtcCallService::class.java)
.setAction(ACTION_LOCAL_HANGUP)
)
}, WebRtcCallActivity.BUSY_SIGNAL_DELAY_FINISH)
}
private fun handleIncomingCall(intent: Intent) {
if (callManager.currentConnectionState != STATE_IDLE) throw IllegalStateException("Incoming on non-idle")
val offer = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION)
callManager.postConnectionEvent(STATE_ANSWERING)
callManager.callId = getCallId(intent)
callManager.clearPendingIceUpdates()
val recipient = getRemoteRecipient(intent)
callManager.recipient = recipient
if (isIncomingMessageExpired(intent)) {
insertMissedCall(recipient, true)
}
}
private fun handleCheckTimeout(intent: Intent) {
val callId = callManager.callId ?: return
val callState = callManager.currentConnectionState
if (callId == getCallId(intent) && callState != STATE_CONNECTED) {
Log.w(TAG, "Timing out call: $callId")
callManager.postViewModelState(CallViewModel.State.CALL_DISCONNECTED)
}
}
private fun setCallInProgressNotification(type: Int, recipient: Recipient?) {
startForeground(
CallNotificationBuilder.WEBRTC_NOTIFICATION,
CallNotificationBuilder.getCallInProgressNotification(this, type, recipient)
)
}
private fun getRemoteRecipient(intent: Intent): Recipient {
val remoteAddress = intent.getParcelableExtra<Address>(EXTRA_RECIPIENT_ADDRESS)
?: throw AssertionError("No recipient in intent!")
return Recipient.from(this, remoteAddress, true)
}
private fun getCallId(intent: Intent) : UUID {
return intent.getSerializableExtra(EXTRA_CALL_ID) as? UUID
?: throw AssertionError("No callId in intent!")
}
private fun insertMissedCall(recipient: Recipient, signal: Boolean) {
// TODO
// val messageAndThreadId = DatabaseComponent.get(this).smsDatabase().insertReceivedCall(recipient.address)
// MessageNotifier.updateNotification(this, messageAndThreadId.second, signal)
}
private fun isIncomingMessageExpired(intent: Intent) =
System.currentTimeMillis() - intent.getLongExtra(EXTRA_TIMESTAMP, -1) > TimeUnit.MINUTES.toMillis(2)
override fun onDestroy() {
super.onDestroy()
callReceiver?.let { receiver ->

View File

@@ -0,0 +1,107 @@
package org.thoughtcrime.securesms.util
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import network.loki.messenger.R
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.service.WebRtcCallService
class CallNotificationBuilder {
companion object {
const val WEBRTC_NOTIFICATION = 313388
const val TYPE_INCOMING_RINGING = 1
const val TYPE_OUTGOING_RINGING = 2
const val TYPE_ESTABLISHED = 3
const val TYPE_INCOMING_CONNECTING = 4
@JvmStatic
fun getCallInProgressNotification(context: Context, type: Int, recipient: Recipient?): Notification {
val contentIntent = Intent(context, WebRtcCallActivity::class.java)
.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, 0)
val builder = NotificationCompat.Builder(context, NotificationChannels.CALLS)
.setSmallIcon(R.drawable.ic_baseline_call_24)
.setContentIntent(pendingIntent)
.setOngoing(true)
recipient?.name?.let { name ->
builder.setContentTitle(name)
}
when (type) {
TYPE_INCOMING_CONNECTING -> {
builder.setContentText(context.getString(R.string.CallNotificationBuilder_connecting))
builder.priority = NotificationCompat.PRIORITY_MIN
}
TYPE_INCOMING_RINGING -> {
builder.setContentText(context.getString(R.string.NotificationBarManager__incoming_signal_call))
builder.addAction(getServiceNotificationAction(
context,
WebRtcCallService.ACTION_DENY_CALL,
R.drawable.ic_close_grey600_32dp,
R.string.NotificationBarManager__deny_call
))
builder.addAction(getActivityNotificationAction(
context,
WebRtcCallActivity.ACTION_ANSWER,
R.drawable.ic_phone_grey600_32dp,
R.string.NotificationBarManager__answer_call
))
}
TYPE_OUTGOING_RINGING -> {
builder.setContentText(context.getString(R.string.NotificationBarManager__establishing_signal_call))
builder.addAction(getServiceNotificationAction(
context,
WebRtcCallService.ACTION_LOCAL_HANGUP,
R.drawable.ic_call_end_grey600_32dp,
R.string.NotificationBarManager__cancel_call
))
}
else -> {
builder.setContentText(context.getString(R.string.NotificationBarManager_call_in_progress))
builder.addAction(getServiceNotificationAction(
context,
WebRtcCallService.ACTION_LOCAL_HANGUP,
R.drawable.ic_call_end_grey600_32dp,
R.string.NotificationBarManager__end_call
))
}
}
return builder.build()
}
@JvmStatic
private fun getServiceNotificationAction(context: Context, action: String, iconResId: Int, titleResId: Int): NotificationCompat.Action {
val intent = Intent(context, WebRtcCallService::class.java)
.setAction(action)
val pendingIntent = PendingIntent.getService(context, 0, intent, 0)
return NotificationCompat.Action(iconResId, context.getString(titleResId), pendingIntent)
}
@JvmStatic
private fun getActivityNotificationAction(context: Context, action: String,
@DrawableRes iconResId: Int, @StringRes titleResId: Int): NotificationCompat.Action {
val intent = Intent(context, WebRtcCallActivity::class.java)
.setAction(action)
val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
return NotificationCompat.Action(iconResId, context.getString(titleResId), pendingIntent)
}
}
}

View File

@@ -15,7 +15,7 @@ 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.recipients.Recipient
import org.thoughtcrime.securesms.calls.WebRtcTestsActivity
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
import org.thoughtcrime.securesms.mms.GlideApp
import java.util.*
@@ -54,13 +54,13 @@ class CallBottomSheet: BottomSheetDialogFragment() {
nameTextView.text = recipient.name ?: address.serialize()
acceptButton.setOnClickListener {
val intent = Intent(requireContext(), WebRtcTestsActivity::class.java)
val intent = Intent(requireContext(), WebRtcCallActivity::class.java)
val bundle = bundleOf(
WebRtcTestsActivity.EXTRA_ADDRESS to address,
WebRtcTestsActivity.EXTRA_CALL_ID to callId
WebRtcCallActivity.EXTRA_ADDRESS to address,
WebRtcCallActivity.EXTRA_CALL_ID to callId
)
intent.action = WebRtcTestsActivity.ACTION_ANSWER
bundle.putStringArray(WebRtcTestsActivity.EXTRA_SDP, sdp)
intent.action = WebRtcCallActivity.ACTION_ANSWER
bundle.putStringArray(WebRtcCallActivity.EXTRA_SDP, sdp)
intent.putExtras(bundle)
startActivity(intent)

View File

@@ -11,6 +11,7 @@ import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.service.WebRtcCallService
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
import org.thoughtcrime.securesms.webrtc.video.CameraState
import org.webrtc.*
@@ -59,19 +60,21 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
val remoteVideoEvents = _remoteVideoEvents.asSharedFlow()
private val _connectionEvents = MutableStateFlow<StateEvent>(StateEvent.CallStateUpdate(CallState.STATE_IDLE))
val connectionEvents = _connectionEvents.asSharedFlow()
private val _callStateEvents = MutableStateFlow(CallViewModel.State.CALL_PENDING)
val callStateEvents = _callStateEvents.asSharedFlow()
private var localCameraState: CameraState = CameraState.UNKNOWN
private var microphoneEnabled = true
private var remoteVideoEnabled = false
private var bluetoothAvailable = false
private val currentCallState = (_connectionEvents.value as StateEvent.CallStateUpdate).state
val currentConnectionState = (_connectionEvents.value as StateEvent.CallStateUpdate).state
private val networkExecutor = Executors.newSingleThreadExecutor()
private var eglBase: EglBase? = null
private var callId: UUID? = null
private var recipient: Recipient? = null
var callId: UUID? = null
var recipient: Recipient? = null
private var peerConnectionWrapper: PeerConnectionWrapper? = null
private var dataChannel: DataChannel? = null
@@ -82,6 +85,23 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
private var remoteRenderer: SurfaceViewRenderer? = null
private var peerConnectionFactory: PeerConnectionFactory? = null
fun clearPendingIceUpdates() {
pendingOutgoingIceUpdates.clear()
pendingIncomingIceUpdates.clear()
}
fun startOutgoingRinger(ringerType: OutgoingRinger.Type) {
signalAudioManager.startOutgoingRinger(ringerType)
}
fun postConnectionEvent(newState: CallState) {
_connectionEvents.value = StateEvent.CallStateUpdate(newState)
}
fun postViewModelState(newState: CallViewModel.State) {
_callStateEvents.value = newState
}
private fun createCameraCapturer(enumerator: CameraEnumerator): CameraVideoCapturer? {
val deviceNames = enumerator.deviceNames
@@ -128,7 +148,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
}
fun isBusy(context: Context) = currentCallState != CallState.STATE_IDLE
fun isBusy(context: Context) = currentConnectionState != CallState.STATE_IDLE
|| context.getSystemService(TelephonyManager::class.java).callState != TelephonyManager.CALL_STATE_IDLE
fun initializeVideo(context: Context) {
@@ -162,14 +182,14 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
}
fun setAudioEnabled(isEnabled: Boolean) {
currentCallState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) {
currentConnectionState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) {
peerConnectionWrapper?.setAudioEnabled(isEnabled)
_audioEvents.value = StateEvent.AudioEnabled(true)
}
}
fun setVideoEnabled(isEnabled: Boolean) {
currentCallState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) {
currentConnectionState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) {
peerConnectionWrapper?.setVideoEnabled(isEnabled)
_audioEvents.value = StateEvent.AudioEnabled(true)
}
@@ -236,7 +256,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
}
fun stop() {
signalAudioManager.stop(currentCallState in OUTGOING_STATES)
signalAudioManager.stop(currentConnectionState in OUTGOING_STATES)
peerConnectionWrapper?.dispose()
peerConnectionWrapper = null

View File

@@ -14,15 +14,30 @@ import javax.inject.Inject
@HiltViewModel
class CallViewModel @Inject constructor(private val callManager: CallManager): ViewModel() {
enum class State {
CALL_PENDING,
CALL_INCOMING,
CALL_OUTGOING,
CALL_CONNECTED,
CALL_RINGING,
CALL_BUSY,
CALL_DISCONNECTED,
NETWORK_FAILURE,
RECIPIENT_UNAVAILABLE,
NO_SUCH_USER,
UNTRUSTED_IDENTITY,
}
val localAudioEnabledState = callManager.audioEvents.map { it.isEnabled }
val localVideoEnabledState = callManager.videoEvents.map { it.isEnabled }
val remoteVideoEnabledState = callManager.remoteVideoEvents.map { it.isEnabled }
val callState = callManager.callStateEvents
// set up listeners for establishing connection toggling video / audio
init {
callManager.audioEvents.onEach { (enabled) -> callManager.setAudioEnabled(enabled) }
.launchIn(viewModelScope)
callManager.videoEvents.onEach { (enabled) -> callManager.setVideoEnabled(enabled) }
.launchIn(viewModelScope)
}
}

View File

@@ -58,8 +58,8 @@ class SignalAudioManager(private val context: Context,
private val connectedSoundId = soundPool.load(context, R.raw.webrtc_completed, 1)
private val disconnectedSoundId = soundPool.load(context, R.raw.webrtc_disconnected, 1)
private val incomingRinger = IncomingRinger(context)
private val outgoingRinger = OutgoingRinger(context)
val incomingRinger = IncomingRinger(context)
val outgoingRinger = OutgoingRinger(context)
private var wiredHeadsetReceiver: WiredHeadsetReceiver? = null
@@ -340,7 +340,7 @@ class SignalAudioManager(private val context: Context,
incomingRinger.stop()
}
private fun startOutgoingRinger() {
fun startOutgoingRinger(type: OutgoingRinger.Type) {
Log.i(TAG, "startOutgoingRinger(): currentDevice: $selectedAudioDevice")
androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION

View File

@@ -903,5 +903,13 @@
<string name="activity_settings_support">Debug Log</string>
<string name="dialog_share_logs_title">Share Logs</string>
<string name="dialog_share_logs_explanation">Would you like to export your application logs to be able to share for troubleshooting?</string>
<string name="CallNotificationBuilder_connecting">Connecting…</string>
<string name="NotificationBarManager__incoming_signal_call">Incoming call</string>
<string name="NotificationBarManager__deny_call">Deny call</string>
<string name="NotificationBarManager__answer_call">Answer call</string>
<string name="NotificationBarManager_call_in_progress">Call in progress</string>
<string name="NotificationBarManager__cancel_call">Cancel call</string>
<string name="NotificationBarManager__establishing_signal_call">Establishing call</string>
<string name="NotificationBarManager__end_call">End call</string>
</resources>