From d3a2282f03178109003a01d20d2a6a393d5e221e Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Wed, 2 Feb 2022 11:36:08 +0200 Subject: [PATCH] Added Telephony Provider account configuration item Added support for Contact tel URIs Show account's STUN configuration items only if needed Avoid some lint warnings in baresip.c --- app/src/main/cpp/baresip.c | 4 +- .../main/kotlin/com/tutpro/baresip/Account.kt | 9 ++ .../com/tutpro/baresip/AccountActivity.kt | 44 ++++-- .../com/tutpro/baresip/ChatsActivity.kt | 17 +-- .../com/tutpro/baresip/ContactActivity.kt | 35 +++-- .../com/tutpro/baresip/ContactListAdapter.kt | 23 ++- .../com/tutpro/baresip/ContactsActivity.kt | 26 ++-- .../kotlin/com/tutpro/baresip/MainActivity.kt | 67 ++++++--- .../main/kotlin/com/tutpro/baresip/Utils.kt | 63 ++++++-- app/src/main/res/layout/activity_account.xml | 139 +++++++++++------- app/src/main/res/layout/activity_contact.xml | 6 +- app/src/main/res/values/strings.xml | 14 +- 12 files changed, 291 insertions(+), 156 deletions(-) diff --git a/app/src/main/cpp/baresip.c b/app/src/main/cpp/baresip.c index 6d7c1ac6..676732b9 100644 --- a/app/src/main/cpp/baresip.c +++ b/app/src/main/cpp/baresip.c @@ -362,8 +362,8 @@ static struct mqueue *mq; static void mqueue_handler(int id, void *data, void *arg) { if (id == ID_UA_STOP_ALL) { - LOGD("calling ua_stop_all with force %u\n", (unsigned)data); - ua_stop_all((bool)data); + LOGD("calling ua_stop_all with force %u\n", (unsigned)(uintptr_t)data); + ua_stop_all((bool)(uintptr_t)data); } } diff --git a/app/src/main/kotlin/com/tutpro/baresip/Account.kt b/app/src/main/kotlin/com/tutpro/baresip/Account.kt index 6077ee77..e41fed73 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Account.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Account.kt @@ -3,6 +3,8 @@ package com.tutpro.baresip import android.content.Context import java.util.* import kotlin.collections.ArrayList +import java.net.URLEncoder +import java.net.URLDecoder class Account(val accp: String) { @@ -28,6 +30,7 @@ class Account(val accp: String) { var missedCalls = false var unreadMessages = false var callHistory = true + var telProvider = Utils.aorDomain(aor) var resumeUri = "" init { @@ -56,6 +59,10 @@ class Account(val accp: String) { val extra = Api.account_extra(accp) callHistory = Utils.paramValue(extra,"call_history") == "" + telProvider = if (Utils.paramExists(extra, "tel_provider")) + URLDecoder.decode(Utils.paramValue(extra,"tel_provider"), "UTF-8") + else + Utils.aorDomain(aor) } @@ -119,6 +126,8 @@ class Account(val accp: String) { if (!callHistory) extra += ";call_history=no" + extra += ";tel_provider=${URLEncoder.encode(telProvider, "UTF-8")}" + if (extra != "") res += ";extra=\"" + extra.substringAfter(";") + "\"" diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountActivity.kt index 4c8cd209..f8f6982a 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AccountActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AccountActivity.kt @@ -2,10 +2,13 @@ package com.tutpro.baresip import android.app.Activity import android.content.Intent +import android.opengl.Visibility import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE import android.widget.* import androidx.appcompat.app.AppCompatActivity import com.tutpro.baresip.databinding.ActivityAccountBinding @@ -33,6 +36,7 @@ class AccountActivity : AppCompatActivity() { private lateinit var outbound1: EditText private lateinit var outbound2: EditText private lateinit var mediaNat: String + private lateinit var stun: LinearLayout private lateinit var stunServer: EditText private lateinit var stunUser: EditText private lateinit var stunPass: EditText @@ -45,6 +49,7 @@ class AccountActivity : AppCompatActivity() { private var answerMode = Api.ANSWERMODE_MANUAL private lateinit var answerModeSpinner: Spinner private lateinit var vmUri: EditText + private lateinit var telProvider: EditText private lateinit var defaultCheck: CheckBox private val mediaEncKeys = arrayListOf("zrtp", "dtls_srtp", "srtp-mandf", "srtp-mand", "srtp", "") @@ -73,6 +78,7 @@ class AccountActivity : AppCompatActivity() { outbound2 = binding.Outbound2 regCheck = binding.Register mediaNatSpinner = binding.mediaNatSpinner + stun = binding.Stun stunServer = binding.StunServer stunUser = binding.StunUser stunPass = binding.StunPass @@ -80,6 +86,7 @@ class AccountActivity : AppCompatActivity() { dtmfModeSpinner = binding.dtmfModeSpinner answerModeSpinner = binding.answerModeSpinner vmUri = binding.voicemailUri + telProvider = binding.telephonyProvider defaultCheck = binding.Default aor = intent.getStringExtra("aor")!! @@ -230,27 +237,21 @@ class AccountActivity : AppCompatActivity() { stunServer.setText(resources.getString(R.string.stun_server_default)) else if ((mediaNat == "ice") && (stunServer.text.toString() == "")) stunServer.setText(resources.getString(R.string.stun_server_default)) - else if (mediaNat == "") { + if (mediaNat == "") { + stun.visibility = GONE stunServer.setText("") stunUser.setText("") stunPass.setText("") - } - stunServer.isEnabled = mediaNat != "" - stunUser.isEnabled = mediaNat != "" - stunPass.isEnabled = mediaNat != "" + } else + stun.visibility = VISIBLE } override fun onNothingSelected(parent: AdapterView<*>) { } } stunServer.setText(acc.stunServer) - stunServer.isEnabled = mediaNat != "" - stunUser.setText(acc.stunUser) - stunUser.isEnabled = mediaNat != "" - stunPass.setText(acc.stunPass) - stunPass.isEnabled = mediaNat != "" mediaEnc = acc.mediaEnc keyIx = mediaEncKeys.indexOf(mediaEnc) @@ -313,6 +314,8 @@ class AccountActivity : AppCompatActivity() { } } + telProvider.setText(acc.telProvider) + vmUri.setText(acc.vmUri) defaultCheck.isChecked = uaIndex == 0 @@ -563,9 +566,9 @@ class AccountActivity : AppCompatActivity() { if (tVmUri != "") { if (!tVmUri.startsWith("sip:")) tVmUri = "sip:$tVmUri" if (!tVmUri.contains("@")) tVmUri = "$tVmUri@${acc.host()}" - if (!Utils.checkSipUri(tVmUri)) { + if (!Utils.checkUri(tVmUri)) { Utils.alertView(this, getString(R.string.notice), - String.format(getString(R.string.invalid_voicemail_uri), tVmUri)) + String.format(getString(R.string.invalid_sip_or_tel_uri), tVmUri)) return false } Api.account_set_mwi(acc.accp, "yes") @@ -576,6 +579,17 @@ class AccountActivity : AppCompatActivity() { save = true } + val hostPart = telProvider.text.toString().trim() + if (hostPart != acc.telProvider) { + if (hostPart != "" && !Utils.checkHostPortParams(hostPart)) { + Utils.alertView(this, getString(R.string.notice), + String.format(getString(R.string.invalid_sip_uri_hostpart), hostPart)) + return false + } + acc.telProvider = hostPart + save = true + } + if (defaultCheck.isChecked && (uaIndex > 0)) { val uasTmp = BaresipService.uas[0] val statusTmp = BaresipService.status[0] @@ -708,6 +722,12 @@ class AccountActivity : AppCompatActivity() { getString(R.string.voicemain_uri_help) ) } + binding.TelephonyProviderTitle.setOnClickListener { + Utils.alertView( + this, getString(R.string.telephony_provider), + getString(R.string.telephony_provider_help) + ) + } binding.DefaultTitle.setOnClickListener { Utils.alertView( this, getString(R.string.default_account), diff --git a/app/src/main/kotlin/com/tutpro/baresip/ChatsActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/ChatsActivity.kt index faf9f1ae..6298f73f 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ChatsActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ChatsActivity.kt @@ -123,17 +123,14 @@ class ChatsActivity: AppCompatActivity() { plusButton.setOnClickListener { val uriText = peerUri.text.toString().trim() if (uriText.isNotEmpty()) { - var uri = ContactsActivity.findContactURI(uriText) - if (!uri.startsWith("sip:")) { - uri = "sip:$uri" - if (!uri.contains("@")) { - val host = aor.substring(aor.indexOf("@") + 1) - uri = "$uri@$host" - } - } - if (!Utils.checkSipUri(uri)) { + val contactUri = ContactsActivity.findContactUri(uriText) + val uri = if (contactUri == null) + Utils.uriComplete(uriText, Utils.aorDomain(aor)) + else + uriText + if (!Utils.checkUri(uri)) { Utils.alertView(this, getString(R.string.notice), - getString(R.string.invalid_chat_peer_uri)) + String.format(getString(R.string.invalid_sip_or_tel_uri), uri)) } else { peerUri.text.clear() peerUri.isCursorVisible = false diff --git a/app/src/main/kotlin/com/tutpro/baresip/ContactActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/ContactActivity.kt index ffb66fb3..bc29441e 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ContactActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ContactActivity.kt @@ -33,6 +33,7 @@ import com.tutpro.baresip.databinding.ActivityContactBinding import java.io.ByteArrayOutputStream import java.io.File + class ContactActivity : AppCompatActivity() { private lateinit var binding: ActivityContactBinding @@ -83,12 +84,10 @@ class ContactActivity : AppCompatActivity() { nameView.hint = getString(R.string.contact_name) nameView.setSelection(nameView.text.length) val uri = intent.getStringExtra("uri")!! - if (uri == "") { + if (uri == "") uriView.setText("") - uriView.hint = getString(R.string.sip_uri) - } else { + else uriView.setText(uri) - } uOrI = uri androidCheck.isChecked = false } else { @@ -183,6 +182,9 @@ class ContactActivity : AppCompatActivity() { requestPermissions(permissions, CONTACT_PERMISSION_REQUEST_CODE) } + Log.d(TAG, "********** Android sip contacts ${logAndroidContacts(this, "sip")}") + Log.d(TAG, "********** Android tel contacts ${logAndroidContacts(this, "tel")}") + Utils.addActivity("contact,$newContact,$uOrI") } @@ -255,7 +257,7 @@ class ContactActivity : AppCompatActivity() { R.id.checkIcon -> { var newName = nameView.text.toString().trim() - var newUri = uriView.text.toString().trim() + var newUri = uriView.text.toString().filterNot{setOf('-', ' ').contains(it)} if (newName == "") newName = newUri @@ -275,15 +277,14 @@ class ContactActivity : AppCompatActivity() { return false } - if (!newUri.startsWith("sip:")) { - newUri = if (newUri.contains("@") || (BaresipService.uas.size != 1)) - "sip:$newUri" + if (!newUri.startsWith("sip:") && !newUri.startsWith("tel:")) + newUri = if (Utils.isTelNumber(newUri)) + "tel:$newUri" else - "sip:$newUri@${Utils.aorDomain(BaresipService.uas[0].account.aor)}" - } - if (!Utils.checkSipUri(newUri)) { + "sip:$newUri" + if (!Utils.checkUri(newUri)) { Utils.alertView(this, getString(R.string.notice), - String.format(getString(R.string.invalid_contact_uri), newUri)) + String.format(getString(R.string.invalid_sip_or_tel_uri), newUri)) return false } @@ -555,13 +556,16 @@ class ContactActivity : AppCompatActivity() { } @Suppress("UNUSED") - fun logAndroidSipContacts(ctx: Context): HashMap> { + fun logAndroidContacts(ctx: Context, type: String): HashMap> { + val mimeType = if (type == "sip") + CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE + else + CommonDataKinds.Phone.CONTENT_ITEM_TYPE val contacts = HashMap>() val projection = arrayOf(ContactsContract.Data.CONTACT_ID, ContactsContract.Data.DISPLAY_NAME, ContactsContract.Data.MIMETYPE, ContactsContract.Data.DATA1, ContactsContract.Data.PHOTO_THUMBNAIL_URI) - val selection = ContactsContract.Data.MIMETYPE + "='" + - CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE + "'" + val selection = ContactsContract.Data.MIMETYPE + "='" + mimeType + "'" val cur: Cursor? = ctx.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, selection, null, null) while (cur != null && cur.moveToNext()) { @@ -574,6 +578,7 @@ class ContactActivity : AppCompatActivity() { when (mime) { CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE -> kind = "sip" CommonDataKinds.Photo.CONTENT_ITEM_TYPE -> kind = "thumb" + CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> kind = "tel" } Log.d(TAG, "got $id, $name, $kind - $data") var info: MutableList diff --git a/app/src/main/kotlin/com/tutpro/baresip/ContactListAdapter.kt b/app/src/main/kotlin/com/tutpro/baresip/ContactListAdapter.kt index 46310191..907c430e 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ContactListAdapter.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ContactListAdapter.kt @@ -11,10 +11,7 @@ import androidx.appcompat.app.AlertDialog import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ArrayAdapter -import android.widget.ImageButton -import android.widget.ImageView -import android.widget.TextView +import android.widget.* import androidx.core.content.ContextCompat.startActivity import java.io.File @@ -79,13 +76,27 @@ class ContactListAdapter(private val ctx: Context, private val rows: ArrayList", "") .replace(">", "") - return name + return null } fun findContact(uri: String): Contact? { for (c in Contact.contacts()) - if ((Utils.uriUserPart(c.uri) == Utils.uriUserPart(uri)) && - (Utils.uriHostPart(c.uri) == Utils.uriHostPart(uri))) + if (Utils.uriMatch(c.uri, uri)) return c return null } @@ -112,17 +111,14 @@ class ContactsActivity : AppCompatActivity() { return false } + // Return contact name of uri or uri itself if contact with uri is not found fun contactName(uri: String): String { - val uriUser = Utils.uriUserPart(uri) - val uriHost = Utils.uriHostPart(uri) - for (c in Contact.contacts()) { - val contactUser = Utils.uriUserPart(c.uri) - if ((contactUser == uriUser) && - (Utils.isE164Number(contactUser) || - (Utils.uriHostPart(c.uri) == uriHost))) - return c.name - } - return uri + val userPart = Utils.uriUserPart(uri) + return if (Utils.isTelNumber(userPart)) + findContact("tel:$userPart")?.name ?: uri + else + findContact(uri)?.name ?: uri } + } } diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt index 86657033..96bc7c6a 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt @@ -774,11 +774,19 @@ class MainActivity : AppCompatActivity() { ua.account.resumeUri = uriStr } "tel" -> { - val acc = BaresipService.uas[0].account - spinToAor(acc.aor) + var account: Account? = null + for (a in Account.accounts()) + if (a.telProvider != "") { + account = a + break + } + if (account == null) { + Log.w(TAG, "No telephony providers for '$uriStr'") + return + } + spinToAor(account.aor) resumeAction = "call" - acc.resumeUri = uriStr.replace("tel", "sip") + "@" + - Utils.uriHostPart(acc.aor) + account.resumeUri = uriStr } else -> { Log.w(TAG, "Unsupported URI scheme ${uri.scheme}") @@ -1369,16 +1377,24 @@ class MainActivity : AppCompatActivity() { setPositiveButton(R.string.transfer) { dialog, _ -> imm.hideSoftInputFromWindow(transferUri.windowToken, 0) dialog.dismiss() - val uriText = transferUri.text.toString().trim() + var uriText = transferUri.text.toString().trim() if (uriText.isNotEmpty()) { - val uri = Utils.uriComplete( - ContactsActivity.findContactURI(uriText) - .filterNot { it.isWhitespace() }, - Utils.aorDomain(ua.account.aor) - ) - if (!Utils.checkSipUri(uri)) { + uriText = ContactsActivity.findContactUri(uriText) ?: uriText + if (Utils.isTelNumber(uriText)) + uriText = "tel:$uriText" + val uri = if (Utils.isTelUri(uriText)) { + if (ua.account.telProvider == "") { + Utils.alertView(this@MainActivity, getString(R.string.notice), + String.format(getString(R.string.no_telephony_provider))) + return@setPositiveButton + } + Utils.telToSip(uriText, ua.account) + } else { + Utils.uriComplete(uriText, Utils.aorDomain(ua.account.aor)) + } + if (!Utils.checkUri(uri)) { Utils.alertView(this@MainActivity, getString(R.string.notice), - String.format(getString(R.string.invalid_sip_uri), uri)) + String.format(getString(R.string.invalid_sip_or_tel_uri), uri)) } else { if (Call.uaCalls(ua, "").size > 0) { Call.uaCalls(ua, "")[0].transfer(uri) @@ -1655,17 +1671,24 @@ class MainActivity : AppCompatActivity() { val ua = UserAgent.uas()[aorSpinner.selectedItemPosition] val aor = ua.account.aor if (Call.calls().isEmpty()) { - val uriText = callUri.text.toString().trim() + var uriText = callUri.text.toString().trim() if (uriText.isNotEmpty()) { - val uri = Utils.uriComplete( - ContactsActivity.findContactURI(uriText) - .filterNot { it.isWhitespace() }, - Utils.aorDomain(aor) - ) - if (!Utils.checkSipUri(uri)) { - Utils.alertView( - this, getString(R.string.notice), - String.format(getString(R.string.invalid_sip_uri), uri) + uriText = ContactsActivity.findContactUri(uriText) ?: uriText + if (Utils.isTelNumber(uriText)) + uriText = "tel:$uriText" + val uri = if (Utils.isTelUri(uriText)) { + if (ua.account.telProvider == "") { + Utils.alertView(this, getString(R.string.notice), + String.format(getString(R.string.no_telephony_provider))) + return + } + Utils.telToSip(uriText, ua.account) + } else { + Utils.uriComplete(uriText, Utils.aorDomain(aor)) + } + if (!Utils.checkUri(uri)) { + Utils.alertView(this, getString(R.string.notice), + String.format(getString(R.string.invalid_sip_or_tel_uri), uri) ) } else { callUri.isFocusable = false diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index 2c384499..9704dc70 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -105,6 +105,15 @@ object Utils { "" } + fun uriMatch(firstUri: String, secondUri: String): Boolean { + if (firstUri.startsWith("tel:")) + return firstUri == secondUri + if (firstUri.startsWith("sip:")) + return uriUserPart(firstUri) == uriUserPart(secondUri) && + uriHostPart(firstUri) == uriHostPart(secondUri) + return false + } + private fun uriParams(uri: String): List { val params = uri.split(";") return if (params.size == 1) listOf() else params.subList(1, params.size) @@ -121,7 +130,7 @@ object Utils { return if (u.contains("@")) { val user = uriUserPart(u) val host = uriHostPart(u) - if (isE164Number(user) || host == domain) + if (isTelNumber(user) || host == domain) user else "$user@$host" @@ -164,10 +173,6 @@ object Utils { checkTransport(uri.substringAfter("?"), setOf("udp", "tcp"))) } - fun isE164Number(no: String): Boolean { - return Regex("^[+][1-9][0-9]{0,14}\$").matches(no) - } - fun checkIpV4(ip: String): Boolean { return Regex("^(([0-1]?[0-9]{1,2}\\.)|(2[0-4][0-9]\\.)|(25[0-5]\\.)){3}(([0-1]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))$").matches(ip) } @@ -256,6 +261,12 @@ object Utils { return "" } + fun paramExists(params: String, name: String): Boolean { + for (param in params.split(";")) + if (param.substringBefore("=") == name) return true + return false + } + fun checkHostPortParams(hpp: String) : Boolean { val restParams = hpp.split(";", limit = 2) return if (restParams.size == 1) @@ -264,18 +275,42 @@ object Utils { checkHostPort(restParams[0]) && checkParams(restParams[1]) } - fun checkSipUri(uri: String): Boolean { - if (!uri.startsWith("sip:")) return false - val userRest = uri.substring(4).split("@") - return when (userRest.size) { - 1 -> - checkHostPortParams(userRest[0]) - 2 -> - checkUriUser(userRest[0]) && checkHostPortParams(userRest[1]) - else -> false + private fun checkSipUri(uri: String): Boolean { + return if (uri.startsWith("sip:")) { + val userRest = uri.substring(4).split("@") + when (userRest.size) { + 1 -> + checkHostPortParams(userRest[0]) + 2 -> + checkUriUser(userRest[0]) && checkHostPortParams(userRest[1]) + else -> false + } + } else { + false } } + fun isTelNumber(no: String): Boolean { + return Regex("^[+][1-9][0-9- ]{0,14}\$").matches(no) + } + + fun isTelUri(uri: String): Boolean { + return uri.startsWith("tel:") && isTelNumber(uri.substring(4)) + } + + fun checkUri(uri: String): Boolean { + return checkSipUri(uri) || isTelUri(uri) + } + + fun telToSip(telUri: String, account: Account): String { + val hostPart = if (account.telProvider != "") + account.telProvider + else + aorDomain(account.aor) + return "sip:" + telUri.substring(4).filterNot{setOf('-', ' ').contains(it)} + + "@" + hostPart + ";user=phone" + } + fun checkName(name: String): Boolean { return name.isNotEmpty() && name == String(name.toByteArray(), Charsets.UTF_8) && name.lines().size == 1 && !name.contains('"') diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index ce6b0210..18a04a53 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -178,69 +178,77 @@ android:paddingTop="8dp" > - - - - - + android:orientation="vertical" + android:layout_height="wrap_content" > - - + + - - + + - - + + - - + + + + + + - - + app:hintEnabled="false" + app:endIconMode="password_toggle"> + + + + + + + + + + + + android:text="@string/sip_or_tel_uri" > Invalid Voicemail URI \'%1$s\' + Telephony Provider + SIP URI host part used in calls to telephone numbers. + Factory default is account\'s domain. If not given, this account cannot be used to call + telephone numbers. + + SIP URI host part + Invalid SIP URI host part \'%1$s\' Default Account If checked, this account is selected when baresip is started. @@ -247,7 +254,7 @@ Today You New Chat Peer - Invalid SIP URI + Invalid chat peer URI Do you want to delete chat with peer \'%1$s\' or add peer to contacts? Do you want to delete chat with \'%1$s\'? @@ -341,6 +348,8 @@ Contact New Contact Name + SIP or tel URI + user@domain or telephone number Invalid contact name \'%1$s\' Contact \'%1$s\' already exists. Invalid SIP URI @@ -353,6 +362,7 @@ Send Message Do you want to delete contact \'%1$s\'? Your maximum number of contacts %1$d has been exceeded. + Telephony provider has not been configured for any account. Alert Info @@ -376,6 +386,7 @@ Confirmation \u2022 %1$s + Invalid SIP or tel URI \'%1$s\' Backup Restore @@ -386,6 +397,7 @@ Call from … Transferring call to … Invalid SIP URI \'%1$s\' + Telephony provider has not been configured for this acccount. Callee Hangup Video call