Compare commits

...

23 Commits

Author SHA1 Message Date
f0a4fd18cd Wear UI: intercept system back so Messages/Conversation/Accounts pop within app
Add BackHandler in WearApp: back from thread: -> messages, messages/accounts
-> dialer. At the root dialer route the handler is disabled so back finishes
the Activity normally. Fixes back-from-Messages dropping to the clock face.
2026-08-21 04:09:41 +01:00
100ee241a2 Wear SIP: plaintext SIP MESSAGE (RFC 3428) send/receive + inbox UI
- Native: add message_send() JNI (Api.message_send) and wire the
  existing message_listen receive callback (message_handler) to forward
  inbound MESSAGEs to WearBaresipService.messageEvent.
- Kotlin: WearBaresipServiceHelper.sendMessage (normalizes peer ->
  sip:peer@mail.txt3.net), MessageStore (JSON file persistence, no extra
  deps), messageEvent stores + posts a CATEGORY_MESSAGE notification.
- UI: MessagesScreen (conversation list) + ConversationScreen (thread +
  send field), reached via a new 'Messages' button on the dialer.
- README roadmap: mark SIP MESSAGE done; reword encryption plan to
  asymmetric E2E over SIP MESSAGE with NFC key exchange (OMEMO is
  XMPP-only and does not apply to SIP).

Verified: builds, app starts, registers over TLS, no crash. Live
two-endpoint exchange still to be tested with a real peer.
2026-08-21 03:48:21 +01:00
0f883c9847 Wear SIP: switch transport to TLS:5062 (verify disabled, mTLS pending)
- accounts: outbound transport=tls on port 5062.
- config.static: tls_verify no + tls_selfsigned yes (server cert is an
  expired Asterisk Private CA; verification disabled for now; mTLS client
  cert support planned but not yet enabled).
- Registration over TLS verified working on Galaxy Watch (Android 16).

Note: TLS transport is independent of the self-managed incoming-call UI
path (verified earlier over UDP), so lock-screen answer/decline + audio
carry over unchanged.
2026-08-21 03:22:04 +01:00
a31d6a693c Wear SIP: incoming call UI over lock screen via self-managed telecom
- Launch InCallActivity from WearConnectionService.onCreateIncomingConnection
  (self-managed telecom context) instead of a background FGS startActivity.
  This is the only path Android permits to show UI over a secure lock screen
  / from the background (BAL exemption); a plain FGS startActivity is blocked.
- Register a SELF_MANAGED PhoneAccount (CAPABILITY_SELF_MANAGED only; a
  self-managed account must not also be CALL_PROVIDER) and hand inbound calls
  to TelecomManager.addNewIncomingCall.
- Answer/Decline routed from the system self-managed Connection back to the
  native stack via WearBaresipServiceHelper (deferred ua_answer/ua_hangup).
- Add dedicated InCallActivity (setShowWhenLocked/turnScreenOn) hosting the
  InCallScreen; MainActivity no longer owns the call UI.
- Switch SIP transport to UDP:5761 (matches server); keep sipnat=outbound +
  natpinhole for NAT traversal.
- Remove dead phoneCall FGS upgrade/downgrade (needed DIALER role; the
  self-managed launch path replaces it).

Verified on Galaxy Watch (Android 16, SDK 36): inbound call rings + shows
InCallActivity over the lock screen (pattern lock on) with working two-way
audio; activity dismisses cleanly on hangup.
2026-08-21 03:15:22 +01:00
899e778e49 Wear SIP: stable inbound calls (ring/answer/hangup) + README
- NAT-survival account settings (sipnat=outbound;natpinhole=yes) so INVITE
  replies are not dropped (was going straight to voicemail).
- Native event_handler emits 'incoming call' (BEVENT_SIPSESS_CONN) carrying
  the SIP msg pointer and 'call incoming' (BEVENT_CALL_INCOMING) carrying the
  real struct call*; passes ua/call as separate jlong args (no lossy string
  parse). uaEvent(String, Long, Long) signature updated.
- Answer flow: ua_accept on 'incoming call' (caller hears ringing) then
  ua_answer deferred to the baresip main loop via a one-shot timer
  (ua_answer_async) so the UI-thread Answer tap does not crash in sip_treplyf.
- FGS started from Application.onCreate (decoupled from Activity) holding
  WifiLock + WakeLock so SIP survives screen-off / task removal.
- Ring: vibration + screen wake + high-priority full-screen-intent
  notification; stopRinging() on established/closed + 30s watchdog so the
  ring never loops forever on remote hang-up.
- Lock-screen presentation: manifest showWhenLocked/turnScreenOn +
  USE_FULL_SCREEN_INTENT permission + InCallScreen launch. (Still unreliable
  when fully asleep + backgrounded — tracked in README.)
- wear/README.md: project overview, how answering works, build/install,
  known gaps, and roadmap (SIP MESSAGE, contacts, start-at-boot, UI).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 01:58:56 +01:00
300bdcc810 Wear SIP: revert to TCP registration (fixes INVITE retransmit timeout)
The UDP switch made the watch register over UDP while Asterisk had
cached a TCP Contact (transport=tcp). Asterisk sent the INVITE over
TCP but got no SIP response (not even 100 Trying) -> 6.4s
retransmission timeout -> immediate voicemail.

Revert to TCP so the watch's registration matches Asterisk's cached
contact. With nat=yes now set, the TCP Contact is made public
(78.141.29.181:<port>), so the INVITE arrives over the persistent TCP
socket and baresip's response reaches Asterisk, letting the call ring
the full configured window instead of timing out.
2026-08-20 23:19:39 +01:00
34661f4ca6 Wear SIP: stop handing incoming calls to system telecom (fixes 2s drop)
The experiment of calling TelecomManager.addNewIncomingCall() for our
COMPANION PhoneAccount caused Samsung Wear's telecom stack to auto-reject
the incoming WearConnection within ~2s; its onReject() then hung up the
native baresip call, so Asterisk dropped to voicemail immediately instead
of ringing for the configured 25s.

Remove the handoff from the 'call incoming' path. The in-app InCallScreen
+ full-screen-intent notification remain the alert UI. The native call now
stays alive and rings for the full window (verified: no more 2s reject +
re-register cycle).
2026-08-20 23:12:23 +01:00
c88bee33d8 Wear SIP: register over UDP (NAT-friendly Contact)
Switch outbound transport from tcp to udp. With Asterisk peer
nat=yes, Asterisk now sends the INVITE to the public source address
(78.141.29.181:<port>) instead of the watch's private Contact, so
incoming calls traverse the NAT and reach the watch.

Verified: external incoming calls now arrive (uaEvent: incoming call
from external caller logged on the watch).
2026-08-20 23:03:15 +01:00
106ef4ed9d Wear SIP: switch account to 01273961147 + NAT-survival settings
- Account line now uses 01273961147 (same credential).
- sipnat=outbound + natpinhole=yes + regint=60 keep the NAT
  mapping/keep-alive fresh so Asterisk's INVITE (sent to the public
  Contact) reaches the watch behind NAT.

Note: sipnat=outbound is ineffective here because the outbound proxy
is the server itself (no edge STUN to learn the watch's public NAT
IP); the actual NAT fix is Asterisk-side (nat=force_rport,comedia on
the peer), which makes Asterisk target the public source address.
2026-08-20 22:40:59 +01:00
bd5b5efa3b Wear SIP: incoming-call alert (full-screen intent + screen wake + vibrate)
- High-importance (IMPORTANCE_MAX) call channel; on incoming call post a
  PRIORITY_MAX / CATEGORY_CALL notification with a full-screen intent that
  launches the InCallScreen over the watch face.
- MainActivity: setShowWhenLocked/setTurnScreenOn + FLAG_SHOW_WHEN_LOCKED
  so the in-call UI presents over the dimmed screen / keyguard.
- Request POST_NOTIFICATIONS at runtime (API 33+ silently drops
  notifications without it) alongside RECORD_AUDIO.
- Vibration ring pattern + wake-lock on incoming call; dismiss the
  heads-up when the call closes.

Note: on Samsung Galaxy Watch the watch notification manager blocks
alerting for apps not in its allowed-notifications list; the device
setting (Settings > Notifications > App notifications > Baresip Wear)
must be enabled for the heads-up to fire. Foreground/app-open case
verified working (call answered, audio -> onboard speaker).
2026-08-20 22:25:42 +01:00
15e266cb43 Wear SIP: audio routing, stock-style dialer, DTMF, incoming-call handling, telecom scaffold
- AudioRouteManager: route to onboard speaker when no BT headset is
  connected, BT SCO when one is; set MODE_IN_COMMUNICATION + focus.
- aaudio: build with AAUDIO_PERFORMANCE_MODE_NONE so Android routing
  (speakerphone) is honoured instead of being bypassed by low-latency.
- DialerScreen: restyled to a stock-dialer layout (large number display,
  3x4 keypad with sub-letters, green call FAB, backspace, recent chips).
- InCallScreen: added DTMF keypad (sendDigit) for voicemail/IVR.
- WearBaresipService: on incoming call, wake screen + vibrate + bring
  activity to foreground so Answer/Decline is visible.
- Fixed dial normalization (bare extension -> full SIP URI with domain)
  and the uap parse in uaEvent so defaultUap is set and calls connect.
- config.static: load g711/opus codecs to resolve 488 Not Acceptable Here.
- Telecom scaffold (experiment): PhoneAccount + ConnectionService handed
  incoming calls via addNewIncomingCall to test stock Wear dialer pickup.
- Submodule libbaresip-android rebuilt with g711/opus/aaudio changes.

Verified: assembleDebug green, installs and registers on Galaxy Watch,
incoming call event fires and is handled without crashing.
2026-08-20 22:06:15 +01:00
a849d98b5e Wear UI: dialer, in-call, debug accounts, baresip: provisioning
- Wear Compose dialer with recent calls and in-call answer/decline/hangup/mute
- Debug accounts screen (scrollable ScalingLazyColumn) for manual SIP config
- baresip:// provisioning intent mirroring phone app (RSA/AES bundle decrypt)
- Physical button (STEM_1/BACK) returns from Accounts via onKeyDown
- Native event handler now emits uap/callp for incoming/registration events
- Writes accounts/auth files in baresip format; verified REGISTER attempt on watch
2026-08-19 04:05:40 +01:00
9f94e72b1e Add Wear OS SIP client module running baresip NDK stack
- New :wear application module (Wear Compose, minSdk 30)
- Second JNI lib 'wearbaresip' reusing distribution static libs
- Watch-side service initializes baresip via full init path
  (libre_init -> conf_configure -> baresip_init -> ua_init)
- FGS started from MainActivity foreground state with mic type
  only (phoneCall requires default-dialer, rejected on watch)
- Runtime-verified: builds, installs, native lib loads and SIP
  stack starts on armeabi-v7a Wear OS device
2026-08-19 03:22:05 +01:00
2372b7fe05 Add provisioning server docs, agentbridge state, and update submodule 2026-08-18 19:58:11 +01:00
e572b2df53 Ignore nested submodule repos and Android build outputs 2026-08-18 19:54:02 +01:00
ed39535e05 Restore x86_64 debug ABI coverage 2026-08-18 19:41:17 +01:00
77bfe351f2 Add baresip:// provisioning link support via encrypted bundle fetch 2026-08-18 19:35:20 +01:00
11619a9130 New version 89.1.1 2026-08-18 12:54:57 +03:00
196205200c Merge pull request #733 from weblate/weblate-baresip-baresip-app
Translations update from Hosted Weblate
2026-08-18 12:49:53 +03:00
78aab58051 Translated using Weblate (Russian)
Currently translated at 44.4% (177 of 398 strings)

Translation: Baresip/store metadata
Translate-URL: https://hosted.weblate.org/projects/baresip/store-metadata/ru/
2026-08-18 11:45:57 +02:00
06f6705450 Show contact name and SIP URI in call URI field if label has not been given 2026-08-18 12:44:36 +03:00
925f0340d0 Submodule update 2026-08-18 10:34:15 +03:00
79ea123143 Submodule update 2026-08-18 10:16:38 +03:00
122 changed files with 4875 additions and 34 deletions

Binary file not shown.

20
.gitignore vendored
View File

@ -9,10 +9,30 @@ java_*
.gradle .gradle
/local.properties /local.properties
build build
/app/build
/captures /captures
*.apk
*.ap_
*.dex
*.class
.externalNativeBuild .externalNativeBuild
/app/release /app/release
/app/.cxx /app/.cxx
/wear/.cxx
/wear/build
gradle/wrapper/gradle-wrapper.jar gradle/wrapper/gradle-wrapper.jar
/distribution/* /distribution/*
/distribution.video/* /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" applicationId = "com.tutpro.baresip"
minSdk = 28 minSdk = 28
targetSdk = 36 targetSdk = 36
versionCode = 543 versionCode = 544
versionName = "89.1.0" versionName = "89.1.1"
@Suppress("UnstableApiUsage") @Suppress("UnstableApiUsage")
externalNativeBuild { externalNativeBuild {
cmake { cmake {
@ -30,7 +30,9 @@ configure<ApplicationExtension> {
} }
buildTypes { buildTypes {
debug { debug {
ndk { abiFilters.add("x86_64") } ndk {
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86_64"))
}
} }
release { release {
isMinifyEnabled = true isMinifyEnabled = true

View File

@ -85,6 +85,12 @@
<action android:name="android.intent.action.DIAL" /> <action android:name="android.intent.action.DIAL" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </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> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.DIAL" /> <action android:name="android.intent.action.DIAL" />

View File

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

View File

@ -31,6 +31,8 @@ import androidx.activity.viewModels
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
@ -176,20 +178,26 @@ class MainActivity : ComponentActivity() {
atStartup = intent.hasExtra("onStartup") atStartup = intent.hasExtra("onStartup")
when (intent?.action) { if (intent?.action == Intent.ACTION_VIEW &&
ACTION_DIAL, ACTION_CALL, ACTION_VIEW -> "baresip".equals(intent.data?.scheme, ignoreCase = true)
if (BaresipService.isServiceRunning) ) {
callAction( handleProvisioningIntent(this, viewModel, intent)
applicationContext, } else {
viewModel, when (intent?.action) {
intent.data, ACTION_DIAL, ACTION_CALL, ACTION_VIEW ->
if (intent?.action == ACTION_CALL) "call" else "dial" if (BaresipService.isServiceRunning)
) callAction(
else applicationContext,
BaresipService.callActionUri = intent.data viewModel,
.toString().replace("%2B", "+") intent.data,
.replace("%20", "") if (intent?.action == ACTION_CALL) "call" else "dial"
.filterNot{setOf('-', ' ', '(', ')').contains(it)} )
else
BaresipService.callActionUri = intent.data
.toString().replace("%2B", "+")
.replace("%20", "")
.filterNot{setOf('-', ' ', '(', ')').contains(it)}
}
} }
val permissions = if (Build.VERSION.SDK_INT >= 33) val permissions = if (Build.VERSION.SDK_INT >= 33)
@ -370,6 +378,10 @@ class MainActivity : ComponentActivity() {
isCallLogIntent(intent) -> { isCallLogIntent(intent) -> {
handleCallLogIntent() 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) -> { intent.action in listOf(ACTION_DIAL, ACTION_CALL, ACTION_VIEW) -> {
callAction( callAction(
applicationContext, applicationContext,
@ -446,4 +458,36 @@ class MainActivity : ComponentActivity() {
exitProcess(0) 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

View File

@ -8,3 +8,4 @@ android.r8.strictFullModeForKeepRules=false
android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false
kotlin.code.style=official kotlin.code.style=official
org.gradle.jvmargs=-Xmx2G -Dfile.encoding=UTF-8 org.gradle.jvmargs=-Xmx2G -Dfile.encoding=UTF-8
android.suppressUnsupportedCompileSdk=37.0

View File

@ -5,9 +5,10 @@ composeBom = "2026.06.01"
coreKtx = "1.19.0" coreKtx = "1.19.0"
exifinterface = "1.4.2" exifinterface = "1.4.2"
fragmentKtx = "1.8.9" fragmentKtx = "1.8.9"
gradleVersion = "9.3.1" gradleVersion = "9.1.0"
kotlin = "2.4.10" kotlin = "2.4.10"
kotlinStdlibJdk8 = "2.4.0" kotlinStdlibJdk8 = "2.4.0"
android = "2.4.10"
kotlinSerializationPlugin = "2.4.10" kotlinSerializationPlugin = "2.4.10"
kotlinxCoroutinesAndroid = "1.11.0" kotlinxCoroutinesAndroid = "1.11.0"
kotlinxSerializationJson = "1.11.0" kotlinxSerializationJson = "1.11.0"
@ -25,6 +26,8 @@ navigationRuntimeAndroid = "2.9.8"
composeMaterialIcons = "1.7.8" composeMaterialIcons = "1.7.8"
runtime = "1.11.4" runtime = "1.11.4"
uiText = "1.11.4" uiText = "1.11.4"
wear = "1.4.0"
wearCompose = "1.4.1"
[libraries] [libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
@ -35,6 +38,7 @@ androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", vers
androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" } androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycleProcess" } androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycleProcess" }
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" } androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeCompose" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" } androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
@ -54,7 +58,12 @@ androidx-compose-material-icons-core = { group = "androidx.compose.material", na
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "composeMaterialIcons" } androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "composeMaterialIcons" }
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" } androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" }
androidx-compose-ui-text = { group = "androidx.compose.ui", name = "ui-text", version.ref = "uiText" } androidx-compose-ui-text = { group = "androidx.compose.ui", name = "ui-text", version.ref = "uiText" }
androidx-wear = { module = "androidx.wear:wear", version.ref = "wear" }
androidx-wear-compose = { group = "androidx.wear.compose", name = "compose-material", version.ref = "wearCompose" }
androidx-wear-compose-foundation = { group = "androidx.wear.compose", name = "compose-foundation", version.ref = "wearCompose" }
androidx-wear-compose-navigation = { group = "androidx.wear.compose", name = "compose-navigation", version.ref = "wearCompose" }
[plugins] [plugins]
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerializationPlugin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerializationPlugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "android" }

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