Compare commits

..

10 Commits

101 changed files with 854 additions and 28 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

18
.gitignore vendored
View File

@ -9,10 +9,28 @@ java_*
.gradle
/local.properties
build
/app/build
/captures
*.apk
*.ap_
*.dex
*.class
.externalNativeBuild
/app/release
/app/.cxx
gradle/wrapper/gradle-wrapper.jar
/distribution/*
/distribution.video/*
libbaresip-android/amr
libbaresip-android/baresip
libbaresip-android/bcg729
libbaresip-android/codec2
libbaresip-android/g722
libbaresip-android/g7221
libbaresip-android/ilbc
libbaresip-android/openssl
libbaresip-android/opus
libbaresip-android/re
libbaresip-android/sndfile
libbaresip-android/vo-amrwbenc
libbaresip-android/zrtpcpp

132
SERVER.md Normal file
View File

@ -0,0 +1,132 @@
# Provisioning server
Baresip Studio can provision accounts from a server-encrypted bundle using a
per-device RSA key pair generated in Android Keystore.
Endpoint contract
- Base URL: whatever you pass as `endpoint=` in the `baresip:` URI
- Path used by the app: `<endpoint>/bundle`
- Method: POST
- Request Content-Type: application/json
- Response Content-Type: application/json
Enrollment request body
{
"extension": "101",
"public_key": "<base64 DER SubjectPublicKeyInfo>"
}
Provisioning bundle response body
{
"encrypted_key": "<base64 RSA-OAEP ciphertext>",
"iv": "<base64 12-byte GCM nonce>",
"ciphertext": "<base64 AES-GCM ciphertext>",
"tag": "<base64 16-byte GCM tag>"
}
The plaintext AES-GCM payload after decryption should be a JSON object like:
{
"connect_string": "sip:101@pbx.example.com",
"transport": "tls",
"sip_verify_server": "yes",
"username": "101",
"password": "secret",
"display_name": "Extension 101",
"account_name": "Desk Phone",
"outbound1": "sip:pbx.example.com",
"outbound2": "",
"register": true,
"reg_int": 900,
"check_origin": true,
"media_enc": "",
"media_nat": "",
"stun_server": "",
"stun_user": "",
"stun_pass": "",
"rtcp_mux": false,
"rel100": false,
"dtmf_mode": 2,
"answer_mode": 0,
"auto_redirect": false,
"vm_uri": "",
"country_code": "",
"tel_provider": "",
"numeric_keypad": false,
"default_account": true,
"custom_params": "",
"client_cert": "<PEM>",
"client_key": "<PEM>",
"ca_certs": "<PEM>"
}
Python server example
- Requirements: python >= 3.10
- pip install fastapi uvicorn pydantic
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import base64
import json
app = FastAPI()
class EnrollRequest(BaseModel):
extension: str
public_key: str
class BundleResponse(BaseModel):
encrypted_key: str
iv: str
ciphertext: str
tag: str
SERVER_RSA_PRIVATE_KEY_PEM = """-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----"""
def server_private_key():
return serialization.load_pem_private_key(
SERVER_RSA_PRIVATE_KEY_PEM.encode(), password=None
)
@app.post("/bundle", response_model=BundleResponse)
def bundle(req: EnrollRequest):
pub = serialization.load_der_public_key(base64.b64decode(req.public_key))
aes_key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(aes_key)
iv = b"012345678901" # replace with os.urandom(12) in production
payload = {
"connect_string": f"sip:{req.extension}@pbx.example.com",
"transport": "tls",
"sip_verify_server": "yes",
"username": req.extension,
"password": "secret",
"client_cert": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
"client_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
"ca_certs": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
}
plaintext = json.dumps(payload).encode("utf-8")
encrypted = aesgcm.encrypt(iv, plaintext, None)
encrypted_key = server_private_key().encrypt(
aes_key,
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None),
)
ciphertext = encrypted[:-16]
tag = encrypted[-16:]
return BundleResponse(
encrypted_key=base64.b64encode(encrypted_key).decode(),
iv=base64.b64encode(iv).decode(),
ciphertext=base64.b64encode(ciphertext).decode(),
tag=base64.b64encode(tag).decode(),
)
Example baresip URI
baresip://provision?endpoint=https://pbx.example.com&extension=101
Security notes
- Always serve over HTTPS.
- Keep SERVER_RSA_PRIVATE_KEY_PEM secret and offline from the app.
- Treat the bundle URL as a secret; short expiry and single-use are recommended.

View File

@ -13,8 +13,8 @@ configure<ApplicationExtension> {
applicationId = "com.tutpro.baresip"
minSdk = 28
targetSdk = 36
versionCode = 543
versionName = "89.1.0"
versionCode = 544
versionName = "89.1.1"
@Suppress("UnstableApiUsage")
externalNativeBuild {
cmake {
@ -30,7 +30,9 @@ configure<ApplicationExtension> {
}
buildTypes {
debug {
ndk { abiFilters.add("x86_64") }
ndk {
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86_64"))
}
}
release {
isMinifyEnabled = true

View File

@ -85,6 +85,12 @@
<action android:name="android.intent.action.DIAL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="baresip" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.DIAL" />

View File

@ -69,8 +69,9 @@ sealed class Contact {
val name = contactWithUri.first.name()
if (includeLabel) {
val label = contactWithUri.second.label
if (label.isNotEmpty()) return "$name $label"
if (unique && contactWithUri.first.uris().size > 1)
if (label.isNotEmpty() && !listOf("SIP", "TEL").contains(label.uppercase()))
return "$name $label"
if (unique || contactWithUri.first.uris().size > 1)
return "$name ${uri.substringAfter(":")}"
return name
}
@ -83,8 +84,9 @@ sealed class Contact {
val name = telContactWithUri.first.name()
if (includeLabel) {
val label = telContactWithUri.second.label
if (label.isNotEmpty()) return "$name $label"
if (unique && telContactWithUri.first.uris().size > 1)
if (label.isNotEmpty() && !listOf("SIP", "TEL").contains(label.uppercase()))
return "$name $label"
if (unique || telContactWithUri.first.uris().size > 1)
return "$name $userPart"
return name
}

View File

@ -31,6 +31,8 @@ import androidx.activity.viewModels
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
@ -176,20 +178,26 @@ class MainActivity : ComponentActivity() {
atStartup = intent.hasExtra("onStartup")
when (intent?.action) {
ACTION_DIAL, ACTION_CALL, ACTION_VIEW ->
if (BaresipService.isServiceRunning)
callAction(
applicationContext,
viewModel,
intent.data,
if (intent?.action == ACTION_CALL) "call" else "dial"
)
else
BaresipService.callActionUri = intent.data
.toString().replace("%2B", "+")
.replace("%20", "")
.filterNot{setOf('-', ' ', '(', ')').contains(it)}
if (intent?.action == Intent.ACTION_VIEW &&
"baresip".equals(intent.data?.scheme, ignoreCase = true)
) {
handleProvisioningIntent(this, viewModel, intent)
} else {
when (intent?.action) {
ACTION_DIAL, ACTION_CALL, ACTION_VIEW ->
if (BaresipService.isServiceRunning)
callAction(
applicationContext,
viewModel,
intent.data,
if (intent?.action == ACTION_CALL) "call" else "dial"
)
else
BaresipService.callActionUri = intent.data
.toString().replace("%2B", "+")
.replace("%20", "")
.filterNot{setOf('-', ' ', '(', ')').contains(it)}
}
}
val permissions = if (Build.VERSION.SDK_INT >= 33)
@ -370,6 +378,10 @@ class MainActivity : ComponentActivity() {
isCallLogIntent(intent) -> {
handleCallLogIntent()
}
intent.action == Intent.ACTION_VIEW &&
intent.data?.scheme.equals("baresip", ignoreCase = true) -> {
handleProvisioningIntent(applicationContext, viewModel, intent)
}
intent.action in listOf(ACTION_DIAL, ACTION_CALL, ACTION_VIEW) -> {
callAction(
applicationContext,
@ -446,4 +458,36 @@ class MainActivity : ComponentActivity() {
exitProcess(0)
}
private fun handleProvisioningIntent(ctx: Context, viewModel: ViewModel, intent: Intent) {
val data = intent.data ?: run {
Log.w(TAG, "Provisioning intent has no data")
return
}
val endpoint = data.getQueryParameter("endpoint") ?: return
val extension = data.getQueryParameter("extension") ?: return
Log.i(TAG, "Provisioning from $endpoint ext=$extension")
lifecycleScope.launch {
try {
val payload = Provisioning.fetchAndDecryptBundle(ctx, endpoint, extension)
val acc = Provisioning.applyBundle(ctx, payload)
if (acc == null) throw Provisioning.ProvisioningException("Failed to apply bundle")
Log.i(TAG, "Provisioning applied: ${acc.aor}")
viewModel.navigateToHome()
if (BaresipService.isServiceRunning) {
val restart = Intent(ctx, BaresipService::class.java)
restart.action = "Stop"
ContextCompat.startForegroundService(ctx, restart)
} else {
val restartApp = Intent(ctx, MainActivity::class.java)
restartApp.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
ctx.startActivity(restartApp)
exitProcess(0)
}
} catch (e: Provisioning.ProvisioningException) {
Log.e(TAG, "Provisioning failed: ${e.message}")
}
}
}
}

View File

@ -0,0 +1,314 @@
package com.tutpro.baresip
import android.content.Context
import android.net.Uri
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.File
import java.net.URL
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.PrivateKey
import java.security.PublicKey
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
import javax.net.ssl.HttpsURLConnection
object Provisioning {
const val ALIAS = "baresip_provisioning"
private const val RSA_KEY_SIZE = 2048
private const val GCM_TAG_LENGTH = 128
private const val SERVER_TIMEOUT_MS = 20_000
data class BundlePayload(
val connectString: String,
val clientCertPem: String? = null,
val clientKeyPem: String? = null,
val caCertsPem: String? = null,
val sipVerifyServer: String? = null,
val sipTransport: String? = null,
val accountName: String? = null,
val username: String? = null,
val password: String? = null,
val displayName: String? = null,
val outbound1: String? = null,
val outbound2: String? = null,
val register: Boolean? = null,
val regInt: Int? = null,
val checkOrigin: Boolean? = null,
val mediaEnc: String? = null,
val mediaNat: String? = null,
val stunServer: String? = null,
val stunUser: String? = null,
val stunPass: String? = null,
val rtcpMux: Boolean? = null,
val rel100: Boolean? = null,
val dtmfMode: Int? = null,
val answerMode: Int? = null,
val autoRedirect: Boolean? = null,
val vmUri: String? = null,
val countryCode: String? = null,
val telProvider: String? = null,
val numericKeypad: Boolean? = null,
val defaultAccount: Boolean? = null,
val customParams: String? = null,
)
fun hasKeyPair(): Boolean {
return try {
val ks = KeyStore.getInstance("AndroidKeyStore")
ks.load(null)
ks.containsAlias(ALIAS)
} catch (e: Exception) {
Log.w(TAG, "hasKeyPair check failed: ${e.message}")
false
}
}
suspend fun ensureKeyPair(): PublicKey = withContext(Dispatchers.IO) {
try {
val ks = KeyStore.getInstance("AndroidKeyStore")
ks.load(null)
if (ks.containsAlias(ALIAS)) {
val pub = ks.getCertificate(ALIAS).publicKey
return@withContext pub
}
} catch (e: Exception) {
Log.w(TAG, "ensureKeyPair reload failed: ${e.message}")
}
val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA)
val spec = KeyGenParameterSpec.Builder(
ALIAS,
KeyProperties.PURPOSE_DECRYPT or KeyProperties.PURPOSE_ENCRYPT
)
.setAlgorithmParameterSpec(java.security.spec.RSAKeyGenParameterSpec(RSA_KEY_SIZE, java.security.spec.RSAKeyGenParameterSpec.F4))
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
.setRandomizedEncryptionRequired(true)
.build()
kpg.initialize(spec)
val kp = kpg.generateKeyPair()
kp.public
}
fun publicKeyPem(publicKey: PublicKey): String {
val b64 = Base64.encodeToString(publicKey.encoded, Base64.NO_WRAP)
return "-----BEGIN PUBLIC KEY-----\n$b64\n-----END PUBLIC KEY-----"
}
suspend fun fetchAndDecryptBundle(
ctx: Context,
endpoint: String,
extension: String
): BundlePayload = withContext(Dispatchers.IO) {
try {
val pub = ensureKeyPair()
val enrollment = JSONObject()
enrollment.put("extension", extension)
enrollment.put("public_key", Base64.encodeToString(pub.encoded, Base64.NO_WRAP))
val url = Uri.parse(endpoint).buildUpon()
.appendPath("bundle")
.build()
.toString()
val conn = URL(url).openConnection() as HttpsURLConnection
conn.requestMethod = "POST"
conn.setRequestProperty("Content-Type", "application/json")
conn.doOutput = true
conn.connectTimeout = SERVER_TIMEOUT_MS
conn.readTimeout = SERVER_TIMEOUT_MS
conn.outputStream.use { os ->
os.write(enrollment.toString().toByteArray(Charsets.UTF_8))
os.flush()
}
val code = conn.responseCode
if (code !in 200..299) {
val err = conn.errorStream?.bufferedReader()?.use { it.readText() } ?: "HTTP $code"
throw ProvisioningException("Provisioning failed: $err")
}
val body = conn.inputStream.bufferedReader().use { it.readText() }
val root = JSONObject(body)
val encryptedKeyB64 = root.optString("encrypted_key")
?: throw ProvisioningException("Missing encrypted_key")
val ivB64 = root.optString("iv")
?: throw ProvisioningException("Missing iv")
val ciphertextB64 = root.optString("ciphertext")
?: throw ProvisioningException("Missing ciphertext")
val tagB64 = root.optString("tag")
?: throw ProvisioningException("Missing tag")
val encryptedKey = Base64.decode(encryptedKeyB64, Base64.NO_WRAP)
val iv = Base64.decode(ivB64, Base64.NO_WRAP)
val ciphertext = Base64.decode(ciphertextB64, Base64.NO_WRAP)
val tag = Base64.decode(tagB64, Base64.NO_WRAP)
val aesKey = rsaDecrypt(encryptedKey)
val plaintext = aesGcmDecrypt(aesKey, iv, ciphertext, tag)
val payload = JSONObject(String(plaintext, Charsets.UTF_8))
val connectString = payload.optString("connect_string")
?: throw ProvisioningException("Missing connect_string in bundle")
BundlePayload(
connectString = connectString,
clientCertPem = payload.optString("client_cert", null),
clientKeyPem = payload.optString("client_key", null),
caCertsPem = payload.optString("ca_certs", null),
sipVerifyServer = payload.optString("sip_verify_server"),
sipTransport = payload.optString("transport"),
accountName = payload.optString("account_name"),
username = payload.optString("username"),
password = payload.optString("password"),
displayName = payload.optString("display_name"),
outbound1 = payload.optString("outbound1"),
outbound2 = payload.optString("outbound2"),
register = if (payload.has("register")) payload.getBoolean("register") else null,
regInt = if (payload.has("reg_int")) payload.getInt("reg_int") else null,
checkOrigin = if (payload.has("check_origin")) payload.getBoolean("check_origin") else null,
mediaEnc = payload.optString("media_enc", null),
mediaNat = payload.optString("media_nat", null),
stunServer = payload.optString("stun_server", null),
stunUser = payload.optString("stun_user", null),
stunPass = payload.optString("stun_pass", null),
rtcpMux = if (payload.has("rtcp_mux")) payload.getBoolean("rtcp_mux") else null,
rel100 = if (payload.has("rel100")) payload.getBoolean("rel100") else null,
dtmfMode = if (payload.has("dtmf_mode")) payload.getInt("dtmf_mode") else null,
answerMode = if (payload.has("answer_mode")) payload.getInt("answer_mode") else null,
autoRedirect = if (payload.has("auto_redirect")) payload.getBoolean("auto_redirect") else null,
vmUri = payload.optString("vm_uri", null),
countryCode = payload.optString("country_code", null),
telProvider = payload.optString("tel_provider", null),
numericKeypad = if (payload.has("numeric_keypad")) payload.getBoolean("numeric_keypad") else null,
defaultAccount = if (payload.has("default_account")) payload.getBoolean("default_account") else null,
customParams = payload.optString("custom_params", null),
)
} catch (e: ProvisioningException) {
throw e
} catch (e: Exception) {
Log.e(TAG, "Provisioning fetch failed", e)
throw ProvisioningException("Provisioning error: ${e.message}")
}
}
suspend fun applyBundle(ctx: Context, payload: BundlePayload): Account? = withContext(Dispatchers.IO) {
try {
val filesPath = BaresipService.filesPath
val file = File(filesPath)
if (!file.exists()) file.mkdirs()
var connectString = payload.connectString.trim()
val transport = payload.sipTransport?.trim()?.lowercase()
if ((transport == "tls" || transport == "wss") && !connectString.contains("transport=")) {
connectString = if (connectString.contains("?")) {
"$connectString&transport=$transport"
} else {
"$connectString;transport=$transport"
}
}
val existing = Account.ofAor(connectString)
val ua = if (existing != null) UserAgent.ofAor(existing.aor) else null
val acc = ua?.account ?: run {
val aor = if (connectString.startsWith("sip:")) connectString else "sip:$connectString"
val uap = UserAgent.uaAlloc(
"<$aor>;stunserver=\"stun:stun.l.google.com:19302\";regq=0.5;pubint=0;regint=0;check_origin=no;mwi=no"
)
if (uap == 0L) return@withContext null
UserAgent.ofUap(uap)?.account ?: return@withContext null
}
payload.displayName?.let { acc.displayName = it }
payload.username?.let { acc.authUser = it }
if (!payload.password.isNullOrBlank()) {
BaresipService.aorPasswords[acc.aor] = payload.password
}
payload.outbound1?.let { if (acc.outbound.size > 0) acc.outbound[0] = it else acc.outbound.add(it) }
payload.outbound2?.let { if (acc.outbound.size > 1) acc.outbound[1] = it else if (it.isNotBlank()) acc.outbound.add(it) }
if (payload.register != null) {
acc.regint = if (payload.register) payload.regInt ?: REGISTRATION_INTERVAL else 0
acc.configuredRegInt = payload.regInt ?: REGISTRATION_INTERVAL
}
payload.checkOrigin?.let { acc.checkOrigin = it }
payload.mediaEnc?.let { acc.mediaEnc = it }
payload.mediaNat?.let { acc.mediaNat = it }
payload.stunServer?.let { acc.stunServer = it }
payload.stunUser?.let { acc.stunUser = it }
payload.stunPass?.let { acc.stunPass = it }
payload.rtcpMux?.let { acc.rtcpMux = it }
if (payload.rel100 != null) {
acc.rel100Mode = if (payload.rel100) Api.REL100_ENABLED else Api.REL100_DISABLED
}
payload.dtmfMode?.let { acc.dtmfMode = it }
payload.answerMode?.let { acc.answerMode = it }
payload.autoRedirect?.let { acc.autoRedirect = it }
payload.vmUri?.let { acc.vmUri = it }
payload.countryCode?.let { acc.countryCode = it }
payload.telProvider?.let { acc.telProvider = it }
payload.numericKeypad?.let { acc.numericKeypad = it }
payload.defaultAccount?.let { if (it) UserAgent.ofAor(acc.aor)?.makeDefault() }
payload.customParams?.let { acc.customParams = it }
payload.accountName?.let { acc.nickName = it }
payload.caCertsPem?.let {
File("$filesPath/ca_certs.crt").writeText(it, Charsets.UTF_8)
}
payload.clientCertPem?.let { cert ->
File("$filesPath/cert.pem").writeText(cert, Charsets.UTF_8)
}
payload.clientKeyPem?.let { key ->
File("$filesPath/cert_key.pem").writeText(key, Charsets.UTF_8)
}
if (!payload.sipVerifyServer.isNullOrBlank()) {
val config = File("$filesPath/config")
if (config.exists()) {
val cfg = config.readText(Charsets.UTF_8)
config.writeText(cfg.replace("sip_verify_server ${Config.variable("sip_verify_server")}", "sip_verify_server ${payload.sipVerifyServer}"), Charsets.UTF_8)
}
}
withContext(Dispatchers.Main) {
Account.saveAccounts()
UserAgent.ofAor(acc.aor)?.let { if (acc.regint > 0) it.reRegister() }
}
acc
} catch (e: Exception) {
Log.e(TAG, "Failed to apply bundle", e)
null
}
}
private fun rsaDecrypt(encrypted: ByteArray): ByteArray {
val ks = KeyStore.getInstance("AndroidKeyStore")
ks.load(null)
val privateKey = ks.getKey(ALIAS, null) as PrivateKey
val cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")
cipher.init(Cipher.DECRYPT_MODE, privateKey)
return cipher.doFinal(encrypted)
}
private fun aesGcmDecrypt(key: ByteArray, iv: ByteArray, ciphertext: ByteArray, tag: ByteArray): ByteArray {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), GCMParameterSpec(GCM_TAG_LENGTH, iv))
val combined = ByteArray(ciphertext.size + tag.size)
System.arraycopy(ciphertext, 0, combined, 0, ciphertext.size)
System.arraycopy(tag, 0, combined, ciphertext.size, tag.size)
return cipher.doFinal(combined)
}
class ProvisioningException(message: String) : Exception(message)
}

View File

@ -0,0 +1,2 @@
- Show contact name and SIP URI in call URI field if label has not been given
- iLBC audio codec now supports both 20 and 30 modes

View File

@ -0,0 +1,7 @@
- Звук уведомления при сообщении, даже если baresip открыт.
- Если имя контакта пустое, использовать его SIP-URI AoR.
- Ослаблена проверка имени контакта.
- Исправлена редкая ошибка при выборе аккаунта в списке.
- Запрос разрешения на запись с микрофона при старте baresip (если не выдано или отклонено навсегда).
- Улучшена работа с разрешением на хранилище при бэкапе/восстановлении.
- Разрешение на чтение состояния телефона больше не требуется.

View File

@ -0,0 +1,2 @@
- Всегда уведомлять выпадающий список агента пользователя об изменении набора данных при возобновлении
главной активности без intent.

View File

@ -0,0 +1,4 @@
- Добавлены символьные аватары для контактов, чатов и истории звонков.
- Исправлено сохранение/восстановление контактов с не-ASCII именами.
- Улучшено форматирование дат и времени.
- Добавлено примечание об ограничении, связанном с несколькими сетевыми интерфейсами.

View File

@ -0,0 +1,2 @@
- Добавлены изображения как аватары контактов (альтернатива символьным).
- Небольшие улучшения и исправления интерфейса.

View File

@ -0,0 +1,2 @@
- Добавлена возможность указать транспортный протокол для AoR учётной записи.
- Не показывать порт или транспортный протокол учётной записи, кроме как в окне настроек аккаунта.

View File

@ -0,0 +1,6 @@
- Глобальная настройка "Prefer IPv6" заменена на параметр учётной записи "Prefer IPv6 Media".
- Улучшена обработка изменений сетевого подключения.
- Порт не требуется в исходящем IPv6-URI.
- Сброс аудиофокуса выполняется и при выходе, если не был сделан раньше.
- Добавлена начальная поддержка русского языка.
- Обновлены некоторые норвежские строки.

View File

@ -0,0 +1,2 @@
- Повторная регистрация UA при появлении новой активной сети или
изменении свойств соединения, даже если IP-адреса не изменились.

View File

@ -0,0 +1,4 @@
- Не перерегистрировать учётные записи при изменении свойств соединения, если IP-адреса остались прежними.
- Если baresip не запускается, дать пользователю возможность изменить настройки, которые могли вызвать
проблему.
- Обновления и исправления строк.

View File

@ -0,0 +1,8 @@
- Исправлено сохранение учётной записи с параметром ;transport в AoR.
- Исправлено включение уведомлений при закрытии вызова.
- Белый индикатор статуса, если регистрация не включена.
- Громкость по умолчанию теперь влияет на голосовые вызовы и музыку.
- Улучшен макет и реализация окна учётных записей.
- Отладочные сообщения baresip не показываются, если Debug не включён.
- Активности не пересоздаются при смене ориентации экрана.
- Корректное отображение многострочной информации в чате

View File

@ -0,0 +1,4 @@
- Использовать тип использования вместо типа потока при запросе аудиофокуса.
- Исправлено форматирование английского текста помощи для учётной записи.
- Не показывать возможный порт AoR при запросе на его удаление.
- Индикатор незарегистрированного статуса залит белым цветом.

View File

@ -0,0 +1,5 @@
- Имя пользователя для аутентификации теперь по умолчанию равно имени учётной записи.
- Запрос пароля при старте baresip, если указано имя пользователя, но пароль отсутствует.
- Индикатор статуса учётной записи теперь жёлтый при включённой регистрации в настройках.
- Исправлена ошибка создания первой учётной записи на старых версиях Android.
- Небольшие улучшения текста.

View File

@ -0,0 +1,2 @@
- Не подписываться на индикацию ожидающих сообщений при создании учётной записи, если URI голосовой почты
не задан.

View File

@ -0,0 +1,4 @@
- Предотвращены вылеты при двойном нажатии на иконки и
элементы списка.
- Цвет иконки уведомления учётной записи изменён на белый, если она
не зарегистрирована.

View File

@ -0,0 +1,3 @@
- Добавлен флажок "Показать пароль" в запросе пароля аутентификации.
- Запрос разрешения на микрофон сразу при запуске baresip.
- Запрещён символ " в пароле аутентификации учётной записи.

View File

@ -0,0 +1,3 @@
- Добавлена поддержка Android 10 (API level 29).
- Улучшен запрос разрешения на микрофон при первом запуске baresip.
- Исправлен вылет при нажатии кнопки вызова без учётных записей.

View File

@ -0,0 +1,6 @@
- Из-за ограничений Android 10 baresip больше не может автоматически
запускаться после загрузки без уведомления.
- Исправлен перезапуск baresip на Android 10.
- Удалена настройка режима ICE Lite в связи с удалением в вышестоящей версии.
- Добавлены строки на бразильском португальском из Weblate.
- Небольшие исправления в испанских и норвежских строках.

View File

@ -0,0 +1,3 @@
- Улучшена настройка аудиокодеков учётной записи.
- Всегда показывать входящий вызов (если есть) при возобновлении baresip.
- Улучшено и исправлено возобновление на неосновных активностях.

View File

@ -0,0 +1,3 @@
- Исправлен вылет при возврате к предыдущей активности после
полученного вызова или сообщения.
- Улучшена работа с датчиком приближения и громкой связью.

View File

@ -0,0 +1,4 @@
- Добавлена опция медианата TURN для учётной записи.
- Добавлена возможность указать имя пользователя/пароль для STUN/TURN сервера.
- Строки, связанные с STUN/TURN, для некоторых языков пока не обновлены.
- Дополнительные улучшения диалогов.

View File

@ -0,0 +1,3 @@
- Улучшения подавления акустического эха.
- Настройки звука перенесены из основных настроек в новую активность.
- Добавлена настройка расширенного фильтра AEC.

View File

@ -0,0 +1,2 @@
- Исправлен возврат в Audio Activity после паузы.
- Исправлено добавление аудиомодулей в настройках звука.

View File

@ -0,0 +1,2 @@
- Добавлена начальная поддержка переадресации вызовов, инициируемой baresip.
- Добавлен новый язык — португальский из Weblate.

View File

@ -0,0 +1,4 @@
- Добавлена возможность перерегистрации выбранной учётной записи жестом смахивания вниз.
- Автоматический фокус и показ мягкой клавиатуры при запросе паролей
или адресата переадресации.
- Различные исправления, связанные со строками.

View File

@ -0,0 +1,3 @@
- Добавлен речевой кодек AMR-WB (Adaptive Multi-Rate Wideband).
- Добавлен раздел "Лицензии" в текст "О программе".
- Новые переводы на португальский (Бразилия).

View File

@ -0,0 +1,7 @@
- Добавлен переключатель видимости для паролей аутентификации и STUN/TURN учётной записи
- Предпочтение VPN-интерфейса (если есть) при выборе сетевых интерфейсов
- Примечание в "О программе" о порядке предпочтения интерфейсов
- Улучшена настройка обхода NAT для медиа учётной записи
- STUN-сервер Google по умолчанию также при ICE для обхода NAT
- Увеличены макс. длины имени пользователя аутентификации, STUN/TURN и отображаемого имени до 64 символов
- Добавлены/обновлены переводы на FR, NO и FI

View File

@ -0,0 +1,4 @@
- Добавлен аудиокодек G.729.
- Добавлена настройка включения/отключения трассировки SIP-сообщений в logcat.
- Схемы URI 'stuns' и 'turns' для STUN/TURN сервера пока не поддерживаются.
- Новые переводы на португальский (Бразилия).

View File

@ -0,0 +1,3 @@
- Улучшена обработка клавиш увеличения/уменьшения громкости
- Улучшена работа с громкой связью
- Новые переводы на русский (RU)

View File

@ -0,0 +1,2 @@
- Автоматический показ мягкой клавиатуры DTMF при подключении вызова
- Исправлен вылет при нажатии клавиш громкости вне вызова

View File

@ -0,0 +1,3 @@
- Автоматический показ циферблата только при портретной ориентации устройства
- Не воспроизводить звук ожидания вызова при втором входящем вызове
- Улучшен текст "О программе"

View File

@ -0,0 +1,2 @@
- Исправлено включение тёмной темы при запуске приложения baresip
- Новые переводы (португальский (Бразилия))

View File

@ -0,0 +1,4 @@
- Другие приложения теперь могут видеть baresip как телефонное приложение для URI sip: и tel:
- Улучшено сохранение/восстановление текста URI вызова при паузе главной активности
- Добавлен японский перевод
- Улучшения цветовой гаммы

View File

@ -0,0 +1,2 @@
- Улучшен поиск контакта, соответствующего SIP-URI
- Улучшения стиля панели инструментов и меню

View File

@ -0,0 +1,3 @@
- Добавлена смена учётных записей свайпом влево/вправо
- Добавлена настройка режима DTMF для учётной записи
- Обновление переводов из Weblate (французский)

View File

@ -0,0 +1,3 @@
- Переводы на португальский (Бразилия) из Weblate
- Исправлен тег перевода nb-NO для F-Droid
- Устранены предупреждения о 'дублирующем запросе завершения'

View File

@ -0,0 +1,3 @@
- Исправления ошибок, связанных с чатом, историей звонков и контактами
- Исправлены ошибки автодополнения URI и улучшены связанные с URI проверки
- Переводы из Weblate (французский, португальский (Бразилия))

View File

@ -0,0 +1,2 @@
- Добавлена кнопка циферблата для выбора между цифровой и текстовой клавиатурой.
- Исправлено автодополнение вызываемого абонента на основе контактов.

View File

@ -0,0 +1,5 @@
- Добавлена обработка события неудачной переадресации вызова.
- Исправлен вылет при (пере)запуске устройства.
- Исправлен вылет при первом возврате из учётных записей в главную активность.
- Использован тип ввода "textEmailAddress" для URI учётных записей.
- Изменена важность канала уведомлений по умолчанию на низкую.

View File

@ -0,0 +1,3 @@
- Обновлена целевая версия SDK до API уровня 30
- На Android 10 и выше пользователь выбирает файл при операциях
резервного копирования, восстановления и импорта TLS-сертификатов

View File

@ -0,0 +1,6 @@
- Добавлена поддержка эффективного по пропускной способности режима кодека AMR
- Разрешены экранированные символы в части пользователя SIP URI и имени пользователя аутентификации
- Использование корутины вместо асинхронной задачи для получения конфигурации учётной записи из сети
- На Android 10+ используется выборщик файлов для TLS-сертификата и файлов CA
- Запрос подтверждения сброса настроек к заводским
- Новые переводы (португальский и португальский (Бразилия))

View File

@ -0,0 +1,3 @@
- Воспроизводить звук при автоответе
- Удалён кодек iLBC (удалён из вышестоящей версии)
- Удалены неиспользуемые аудиофайлы

View File

@ -0,0 +1,4 @@
- Улучшен текст "О программе"
- Восстановление позиции списка чатов при возврате из чата
- Исправлен вылет при запросе пароля учётной записи
- Попытка загрузки модуля iLBC не выполняется

View File

@ -0,0 +1,3 @@
- Исправление транспорта websocket в вышестоящей версии
- Новые переводы на португальский (Бразилия)
- Проверка транспорта в URI исходящего прокси

View File

@ -0,0 +1,3 @@
- Улучшения URI STUN/TURN учётной записи
- Различные улучшения, связанные с аудио
- Новые переводы на португальский (Бразилия) и словенский

View File

@ -0,0 +1,2 @@
- Добавлена кнопка включения/отключения микрофона во время вызова
- Использован горизонтальный скролл для кнопок управления вызовом

View File

@ -0,0 +1,4 @@
- Блокировка WiFi, пока baresip использует сеть WiFi
- Восстановлен случайно удалённый файл сигнала обратного вызова
- Добавлено примечание в "О программе" об отключении оптимизации батареи
- Переводы на шведский язык

View File

@ -0,0 +1,4 @@
- Блокировка WiFi при использовании baresip сети WiFi
- Добавлено примечание в "О программе" об оптимизации батареи
- Новые переводы на шведский и португальский (Бразилия)
- Различные улучшения реализации

View File

@ -0,0 +1,2 @@
- Добавлена ограниченная поддержка нескольких одновременно активных сетевых интерфейсов
- Показ уведомления при запуске baresip без доступа к сети

View File

@ -0,0 +1,6 @@
- Использовать фильтр WebRTC Acoustic Echo Cancellation Mobile Mode
- Показывать в уведомлении количество пропущенных вызовов, если их более одного
- При установке громкости вызова по умолчанию устанавливать и громкость медиа, и вызова
- Отменять уведомления при возобновлении главной активности
- Исправлена остановка звонка на старых версиях Android
- Новые переводы на шведский и португальский (Бразилия)

View File

@ -0,0 +1,5 @@
- При включении/отключении aec загружать/выгружать модуль webrtc_aecm
- Использовать BaresipService для воспроизведения всех звуков
- Корректная обработка ответа о ходе выполнения сессии
- Во время вызова клавиши громкости управляют громкостью медиа
- Использовать аудиорежим связи, если не в режиме звонка

View File

@ -0,0 +1,3 @@
- Исправления и улучшения, связанные с аудиофокусом
- Удаление всех пробелов из поля вызываемого абонента
- Новый перевод на португальский (Бразилия)

View File

@ -0,0 +1,5 @@
- Выполнен переход baresip на API уровня 31 (Android 12)
- Улучшено обнаружение доступных сетей
- Улучшены запросы разрешений
- Удалена настройка учётной записи Prefer IPv6 Media
- Новый перевод на португальский (Бразилия)

View File

@ -0,0 +1,4 @@
- Кнопка отключения микрофона перенесена из панели управления вызовом в панель действий
- Добавлена настройка оптимизации батареи
- Запрос разрешения на запись аудио при запуске baresip
- Новые переводы на португальский (Бразилия), французский и шведский

View File

@ -0,0 +1,4 @@
- Кнопка отключения микрофона перенесена из панели управления вызовом в панель действий
- Добавлена настройка оптимизации батареи
- Запрос разрешения на запись аудио при запуске baresip
- Новые переводы на португальский (Бразилия), французский и шведский

View File

@ -0,0 +1,2 @@
- В настоящее время иконка микрофона активна только при подключенном вызове
- Новые переводы на шведский

View File

@ -0,0 +1,5 @@
- Добавлены данные о средней битовой скорости, джиттере, количестве пакетов и потерянных пакетах в информацию о вызове
- Использование иконки паузы акцентного цвета для указания, что пользователь поставил вызов на удержание
- Уведомление пользователя, когда другая сторона поставила вызов на удержание
- Небольшие улучшения, связанные с мягким вводом
- Новые переводы на португальский (Бразилия)

View File

@ -0,0 +1,2 @@
- Улучшена защита приложения baresip при блокировке устройства с защищённой блокировкой
- Новые переводы на португальский (Бразилия)

View File

@ -0,0 +1,4 @@
- Улучшена обработка блокировки/разблокировки устройства
- В настройки добавлена ссылка на настройки Android
- Корректная проверка изменения настройки оптимизации батареи
- Новые переводы на шведский и португальский (Бразилия)

View File

@ -0,0 +1,6 @@
- Улучшено отображение времени в списке истории вызовов
- Добавлена активность с деталями вызова, доступная при касании времени в
списке истории вызовов
- Исправлено отображение аватаров в списках контактов, вызовов и чатов
- Разрешена отмена отвеченного вызова, ожидающего установления
- Новые переводы на португальский (Бразилия), шведский и норвежский

View File

@ -0,0 +1,6 @@
- Улучшена и исправлена обработка событий сообщений и переадресации вызовов
- Показ уведомления о неудачной регистрации в любой текущей активности
- Автоматическое преобразование первых букв предложений в сообщениях в заглавные
- Замена устаревших локальных широковещательных рассылок на события LiveData
- Исправлены некоторые ошибки, связанные с обработкой внешних действий вызова
- Новые переводы на португальский (Бразилия)

View File

@ -0,0 +1,2 @@
- Улучшена реализация контактов, сообщений и истории вызовов.
- Исправлен вылет при получении сообщений до добавления учётных записей.

View File

@ -0,0 +1,2 @@
- Все данные, не связанные с пользовательским интерфейсом, теперь хранятся в сервисе baresip, что позволяет корректно восстанавливаться при завершении других активностей baresip системой Android.
- Множество других улучшений и исправлений ошибок.

View File

@ -0,0 +1,3 @@
- Исправлена критическая ошибка в проверке разрешения на запись аудио,
которая препятствовала запуску новой установки baresip.
- Небольшие улучшения.

View File

@ -0,0 +1,5 @@
- Добавлен пункт конфигурации учётной записи Telephony Provider
- Добавлена поддержка tel URI в качестве значения вызываемого абонента и URI контакта
- Запрос учётной записи Telephony Provider у пользователя, если необходимо для совершения вызова
- Отображение элементов конфигурации STUN учётной записи только при необходимости
- Улучшена обработка действия CALL из других приложений

View File

@ -0,0 +1,5 @@
- Добавлен пункт конфигурации учётной записи Telephony Provider
- Добавлена поддержка tel URI в качестве значения вызываемого абонента и URI контакта
- Запрос учётной записи Telephony Provider у пользователя, если необходимо для совершения вызова
- Отображение элементов конфигурации STUN учётной записи только при необходимости
- Улучшена обработка действия CALL из других приложений

View File

@ -0,0 +1,4 @@
- Не воспроизводить звук занятости или ошибки при завершении вызова
- Повышена надёжность использования Bluetooth
- Улучшена обработка событий от ядра baresip
- Новые переводы на португальский (Бразилия)

View File

@ -0,0 +1,5 @@
- Добавлен хронометр времени вызова в строку "Вызов .../Вызов от ..."
- Добавлена возможность использования контактов Android
- Добавлена настройка "Показывать контакты Android" для выбора отображения контактов Android при нажатии кнопки контактов
- Проверка, что файл восстановления создан приложением baresip
- Новые переводы на шведский и португальский (Бразилия)

View File

@ -0,0 +1,3 @@
- Настройка 'Показывать контакты Android' заменена настройкой 'Контакты', которая позволяет выбрать, какие контакты используются: baresip, Android или оба
- Исправления и улучшения сопоставления имени контакта и URI контакта
- Исправлен вылет при принудительном перезапуске приложения baresip системой Android

View File

@ -0,0 +1,2 @@
- Список выбора контактов теперь содержит только те контакты Android, которые имеют ровно один URI
- Новые переводы на шведский и португальский (Бразилия)

View File

@ -0,0 +1,3 @@
- Исправлено добавление нового контакта baresip
- При выборе новой учётной записи по умолчанию порядок остальных не изменяется
- Улучшена обработка автоматического отклонения вызовов

View File

@ -0,0 +1,4 @@
- Добавлена начальная поддержка сопровождаемой переадресации вызовов
- В активности чата длительное нажатие заменено на короткое
- Текст сообщений теперь можно выделять
- Использование жирного шрифта в тексте выпадающего списка AoR при активном вызове

View File

@ -0,0 +1,4 @@
- Добавлена начальная поддержка сопровождаемой переадресации вызовов
- В активности чата длительное нажатие заменено на короткое
- Текст сообщений теперь можно выделять
- Использование жирного шрифта в тексте выпадающего списка AoR при активном вызове

View File

@ -0,0 +1,2 @@
- Исправлена ошибка, связанная с 32-битной архитектурой armeabi-v7a
- Отображение "Аноним" или "Неизвестно", если хост URI — anonymous.invalid или unknown.invalid

View File

@ -0,0 +1,4 @@
- Отображение переадресующего номера, если входящий вызов был переадресован
- Исправления, связанные с резервным копированием, на старых версиях Android
- Улучшено сопоставление URI с контактом
- Новые переводы на португальский (Бразилия)

View File

@ -0,0 +1,2 @@
- В соответствии с требованием Google, запрашивать согласие пользователя при выборе контактов Android в настройках
- Новые переводы на португальский (Бразилия)

View File

@ -0,0 +1,2 @@
- Исправлена проверка режима "Не беспокоить"
- Новые переводы

View File

@ -0,0 +1,6 @@
- Изменение цвета кнопки вызова на жёлтый, когда кнопка нажата, но вызов ещё не совершён
- Не активировать бесполезное программное подавление эха, если аппаратное недоступно
- Всегда очищать иконку безопасности и поле DTMF при завершении вызова
- Для зарегистрированных учётных записей проверять, что часть пользователя входящего Request URI совпадает с частью пользователя контактного URI REGISTER-запроса
- Улучшения буфера дрожания аудио в вышестоящей версии
- Новые переводы (французский)

View File

@ -0,0 +1,3 @@
- Добавлено примечание о Weblate в текст "О программе"
- Новые переводы (китайский)
- Исправление макроса логирования в библиотеке baresip

View File

@ -0,0 +1,3 @@
- Исправлено добавление нового вызова в историю
- Устранена "вспышка" экрана при звонке из контактов или истории вызовов
- Новые переводы (чешский)

View File

@ -0,0 +1,3 @@
- Добавлен параметр конфигурации учётной записи "Код страны"
- Улучшения, связанные с аудио, из вышестоящей версии
- Новые переводы на португальский (Бразилия) и финский

View File

@ -0,0 +1,3 @@
- Использование протокола установки ключей DTLS на основе эллиптической криптографии
- Исправлено резервное копирование истории вызовов
- Новые переводы на шведский

View File

@ -0,0 +1,3 @@
- Исключить отмеченные звёздочкой контакты Android из режима "Не беспокоить"
- Не терять содержимое нового сообщения, которое ещё не было отправлено
- Новые переводы (шведский)

View File

@ -0,0 +1,3 @@
- Добавлена начальная поддержка динамических цветов на многих экранах
- Улучшено отображение и скрытие мягкой клавиатуры
- Новые переводы (китайский (упрощённое письмо))

View File

@ -0,0 +1,3 @@
- Сохранять изменения учётной записи и настроек до нажатия кнопки "Готово" или "Назад"
- Использовать сертификаты TLS CA File также при предварительной настройке учётной записи из сети
- Не покидать экран настроек в случае ошибки ввода

View File

@ -0,0 +1 @@
- Использование цветовой темы Material3 повсеместно

View File

@ -0,0 +1 @@
- Улучшена видимость поля "Вызов от" на главном экране и поля SIP URI на экране учётной записи

View File

@ -0,0 +1 @@
- Улучшены цвета контейнеров поверхности

View File

@ -0,0 +1,3 @@
- Удалён кодек G.726 из-за большого количества зависимостей
- Добавлен кодек G.722 с меньшим количеством зависимостей
- Новые переводы (шведский, чешский и китайский)

View File

@ -1,9 +1,13 @@
Это приложение SIP-клиент для Android, основанный на <a href="https://github.com/baresip/baresip">baresip</a>.
Это приложение-агент пользователя для Android, основанное на <a href="https://github.com/baresip/baresip">baresip</a> и поддерживающее SIP- и мобильные голосовые вызовы, а также обмен сообщениями.
В настоящее время приложение baresip поддерживает голосовые вызовы, текстовые сообщения, уведомления голосовой почты, а также переводы вызовов «вслепую» и «с участием». Голос может кодироваться с помощью Opus, AMR, GSM, G.729, G.722, G.722.1, G.726 или PCMU/PCMA. Безопасность обеспечивается посредством TLS или WSS для SIP-сигнализации и инкапсуляции мультимедиа в ZRTP или (DTLS) SRTP.
В настоящее время baresip поддерживает голосовые вызовы и сообщения через SIP и мобильные сети. Дополнительно при использовании SIP доступны конференц-связь, индикация ожидающих голосовых сообщений (MWI), а также слепая и сопровождаемая переадресация вызовов. Поддержка MMS-сообщений пока отсутствует.
Разработка приложения baresip обусловлена необходимостью создания безопасного VoIP клиента на основе SIP с открытым исходным кодом для Android, который не зависит от закрытых сторонних сервисов пуш-уведомлений.
В SIP-вызовах аудио может кодироваться с помощью кодеков Opus, AMR, Codec2, G.729, G.722, G.722.1 или PCMU/PCMA. Безопасность обеспечивается за счёт транспорта SIP через TLS или WSS, а также инкапсуляции медиа через ZRTP или SRTP поверх DTLS.
Если вам нужны видеозвонки и имеется устройство с Android версии 9 или новее, поддерживающее API Camera2 на третьем или полном уровне аппаратной поддержки, можете вместо этого приложения установить родственное baresip+.
Разработка baresip обусловлена потребностью в защищённом, открытом SIP-агенте VoIP для Android, который не зависит от проприетарных сторонних сервисов push-уведомлений. Контакты, мобильные вызовы и SMS-сообщения были добавлены, чтобы сократить количество устанавливаемых приложений.
Исходный код доступен на <a href="https://github.com/juha-h/baresip-studio">GitHub</a>, там же можно сообщить о проблемах.
Это приложение можно установить на устройствах с Android 9 и выше. Для мобильных вызовов и обмена сообщениями требуется Android 10 или новее.
Если вам нужны видеозвонки, вместо этого приложения вы можете установить его «родственную» версию — baresip+.
Исходный код доступен на <a href="https://github.com/juha-h/baresip-studio">GitHub</a>, там же можно сообщать об ошибках.

View File

@ -1 +1 @@
VoIP-клиент для Android, основанный на одноимённой SIP-библиотеке
SIP- и мобильный звонки/сообщения, Android-агент на baresip

Some files were not shown because too many files have changed in this diff Show More