diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6a0d90fc..4c2b9d17 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -76,108 +76,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
= 33)
- registerBackInvokedCallback()
- else {
- onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- goBack()
- }
- }
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
- Utils.addActivity("about")
-
- val aboutTitle = getString(R.string.about_title)
- val aboutText = String.format(getString(R.string.about_text),
- BuildConfig.VERSION_NAME)
-
- setContent {
- AppTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = MaterialTheme.colorScheme.background
- ) {
- AboutContent(aboutTitle, aboutText) { goBack() }
- }
- }
- }
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- fun AboutContent(title: String, text: String, navigateBack: () -> Unit) {
- Scaffold(
- modifier = Modifier.safeDrawingPadding(),
- containerColor = LocalCustomColors.current.background,
- topBar = {
- TopAppBar(
- title = {
- Text(text = title,
- color = LocalCustomColors.current.light,
- fontWeight = FontWeight.Bold
- )
- },
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = LocalCustomColors.current.primary
- ),
- navigationIcon = {
- IconButton(onClick = navigateBack) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Localized description",
- tint = LocalCustomColors.current.light
- )
- }
- },
- )
-
- }
- ) { contentPadding ->
- Text(
- text = AnnotatedString.Companion.fromHtml(
- htmlString = text,
- linkStyles = TextLinkStyles(
- style = SpanStyle(color = LocalCustomColors.current.accent)
- )
- ),
- modifier = Modifier
- .padding(horizontal = 16.dp, vertical = 16.dp)
- .padding(contentPadding)
- .verticalScroll(rememberScrollState())
- .fillMaxSize()
- .background(LocalCustomColors.current.background)
- )
- }
- }
-
- override fun onDestroy() {
- if (Build.VERSION.SDK_INT >= 33) {
- if (backInvokedCallback != null)
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
- }
- else
- onBackPressedCallback.remove()
- super.onDestroy()
- }
-
- private fun goBack() {
- BaresipService.activities.remove("about")
- setResult(RESULT_CANCELED, Intent())
- finish()
- }
-
-}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/AboutScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AboutScreen.kt
new file mode 100644
index 00000000..983b57d9
--- /dev/null
+++ b/app/src/main/kotlin/com/tutpro/baresip/AboutScreen.kt
@@ -0,0 +1,96 @@
+package com.tutpro.baresip
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextLinkStyles
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.fromHtml
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+
+fun NavGraphBuilder.aboutScreenRoute(navController: NavController) {
+ composable("about") {
+ AboutScreen(onBack = { navController.popBackStack() })
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun AboutScreen(onBack: () -> Unit) {
+ Scaffold(
+ modifier = Modifier.fillMaxSize().imePadding(),
+ containerColor = LocalCustomColors.current.background,
+ topBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(
+ top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
+ )
+ ) {
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(R.string.about_title),
+ color = LocalCustomColors.current.light,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = null,
+ tint = LocalCustomColors.current.light
+ )
+ }
+ },
+ colors = TopAppBarDefaults.mediumTopAppBarColors(
+ containerColor = LocalCustomColors.current.primary
+ ),
+ windowInsets = WindowInsets(0, 0, 0, 0)
+ )
+ }
+ }
+ ) { contentPadding ->
+ Text(
+ text = AnnotatedString.fromHtml(
+ htmlString = stringResource(R.string.about_text, BuildConfig.VERSION_NAME),
+ linkStyles = TextLinkStyles(SpanStyle(color = LocalCustomColors.current.accent))
+ ),
+ color = LocalCustomColors.current.itemText,
+ modifier = Modifier
+ .padding(horizontal = 16.dp, vertical = 16.dp)
+ .padding(contentPadding)
+ .verticalScroll(rememberScrollState())
+ .fillMaxSize()
+ )
+ }
+}
+
diff --git a/app/src/main/kotlin/com/tutpro/baresip/Account.kt b/app/src/main/kotlin/com/tutpro/baresip/Account.kt
index 75b455f0..e4457b58 100644
--- a/app/src/main/kotlin/com/tutpro/baresip/Account.kt
+++ b/app/src/main/kotlin/com/tutpro/baresip/Account.kt
@@ -249,6 +249,14 @@ class Account(val accp: Long) {
return res
}
+ fun saveAccounts() {
+ var accounts = ""
+ for (a in accounts()) accounts = accounts + a.print() + "\n"
+ Utils.putFileContents(BaresipService.filesPath + "/accounts",
+ accounts.toByteArray(Charsets.UTF_8))
+ // Log.d(TAG, "Saved accounts '${accounts}' to '${BaresipService.filesPath}/accounts'")
+ }
+
fun ofAor(aor: String): Account? {
for (ua in BaresipService.uas.value)
if (ua.account.aor == aor) return ua.account
diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountActivity.kt
deleted file mode 100644
index 53394faf..00000000
--- a/app/src/main/kotlin/com/tutpro/baresip/AccountActivity.kt
+++ /dev/null
@@ -1,1833 +0,0 @@
-package com.tutpro.baresip
-
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import android.os.Bundle
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Switch
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.platform.LocalSoftwareKeyboardController
-import androidx.compose.ui.platform.SoftwareKeyboardController
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.res.vectorResource
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.KeyboardCapitalization
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.text.input.PasswordVisualTransformation
-import androidx.compose.ui.text.input.VisualTransformation
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import com.tutpro.baresip.BaresipService.Companion.uas
-import com.tutpro.baresip.CustomElements.AlertDialog
-import com.tutpro.baresip.CustomElements.LabelText
-import com.tutpro.baresip.CustomElements.verticalScrollbar
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserFactory
-import java.io.StringReader
-import java.net.URL
-import java.util.Locale
-
-class AccountActivity : ComponentActivity() {
-
- private lateinit var mediaEncMap: Map
- private lateinit var mediaNatMap: Map
- private lateinit var dtmfModeMap: Map
- private lateinit var answerModeMap: Map
- private lateinit var redirectModeMap: Map
- private lateinit var acc: Account
- private lateinit var ua: UserAgent
- private lateinit var aor: String
-
- private var kind: String? = null
- private var reRegister = false
- private var oldNickname = ""
- private var newNickname = ""
- private var oldDisplayname = ""
- private var newDisplayname = ""
- private var oldAuthUser = ""
- private var newAuthUser = ""
- private var oldAuthPass = ""
- private var newAuthPass = ""
- private var oldOutbound1 = ""
- private var newOutbound1 = ""
- private var oldOutbound2 = ""
- private var newOutbound2 = ""
- private var oldRegister = false
- private var newRegister = false
- private var oldRegInt = ""
- private var newRegInt = ""
- private var oldMediaEnc = ""
- private var newMediaEnc = ""
- private var oldMediaNat = ""
- private var newMediaNat = ""
- private var oldStunServer = ""
- private var newStunServer = ""
- private var oldStunUser = ""
- private var newStunUser = ""
- private var oldStunPass = ""
- private var newStunPass = ""
- private var oldRtcpMux = false
- private var newRtcpMux = false
- private var old100Rel = false
- private var new100Rel = false
- private var oldDtmfMode = 0
- private var newDtmfMode = 0
- private var oldAnswerMode = 0
- private var newAnswerMode = 0
- private var oldAutoRedirect = false
- private var newAutoRedirect = false
- private var oldVmUri = ""
- private var newVmUri = ""
- private var oldCountryCode = ""
- private var newCountryCode = ""
- private var oldTelProvider = ""
- private var newTelProvider = ""
- private var oldDefaultAccount = false
- private var newDefaultAccount = false
- private var newNumericKeypad = false
- private var password = mutableStateOf("")
- private var showPasswordDialog = mutableStateOf(false)
- private var keyboardController: SoftwareKeyboardController? = null
-
- private val alertTitle = mutableStateOf("")
- private val alertMessage = mutableStateOf("")
- private val showAlert = mutableStateOf(false)
- private val showStun = mutableStateOf(false)
-
- private var backInvokedCallback: OnBackInvokedCallback? = null
- private lateinit var onBackPressedCallback: OnBackPressedCallback
-
- @RequiresApi(33)
- private fun registerBackInvokedCallback() {
- backInvokedCallback = OnBackInvokedCallback { goBack() }
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- backInvokedCallback!!
- )
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
-
- super.onCreate(savedInstanceState)
-
- enableEdgeToEdge()
-
- if (Build.VERSION.SDK_INT >= 33)
- registerBackInvokedCallback()
- else {
- onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- goBack()
- }
- }
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
- aor = intent.getStringExtra("aor")!!
- kind = intent.getStringExtra("kind")
-
- Utils.addActivity("account,$aor")
-
- ua = UserAgent.ofAor(aor)!!
- acc = ua.account
-
- mediaEncMap = mapOf("zrtp" to "ZRTP", "dtls_srtp" to "DTLS-SRTPF",
- "srtp-mand" to "SRTP-MAND", "srtp" to "SRTP", "" to "--")
-
- mediaNatMap = mapOf("stun" to "STUN", "turn" to "TURN", "ice" to "ICE", "" to "--")
-
- dtmfModeMap = mapOf(Api.DTMFMODE_RTP_EVENT to getString(R.string.dtmf_inband),
- Api.DTMFMODE_SIP_INFO to getString(R.string.dtmf_info),
- Api.DTMFMODE_AUTO to getString(R.string.dtmf_auto))
-
- answerModeMap = mapOf(Api.ANSWERMODE_MANUAL to getString(R.string.manual),
- Api.ANSWERMODE_AUTO to getString(R.string.auto))
-
- redirectModeMap = mapOf(false to getString(R.string.manual), true to getString(R.string.auto))
-
- setContent {
- AppTheme {
- keyboardController = LocalSoftwareKeyboardController.current
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = LocalCustomColors.current.background
- ) {
- AccountScreen(kind) { goBack() }
- }
- }
- }
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- fun AccountScreen(kind: String?, navigateBack: () -> Unit) {
-
- var isConfigLoaded by remember { mutableStateOf(false) }
-
- LaunchedEffect(kind, acc) {
- if (kind == "new")
- initAccountFromConfig(acc) { isConfigLoaded = true }
- else
- isConfigLoaded = true
- }
-
- val title = if (acc.nickName.value != "")
- acc.nickName.value
- else
- acc.aor.substringAfter(":")
- Scaffold(
- modifier = Modifier
- .fillMaxHeight()
- .imePadding()
- .safeDrawingPadding(),
- containerColor = LocalCustomColors.current.background,
- topBar = {
- TopAppBar(
- title = {
- Text(
- text = title,
- color = LocalCustomColors.current.light,
- fontWeight = FontWeight.Bold
- )
- },
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = LocalCustomColors.current.primary
- ),
- navigationIcon = {
- IconButton(onClick = navigateBack) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = LocalCustomColors.current.light
- )
- }
- },
- actions = {
- IconButton(onClick = {
- updateAccount()
- }) {
- Icon(
- imageVector = Icons.Filled.Check,
- tint = LocalCustomColors.current.light,
- contentDescription = "Check"
- )
- }
- }
- )
- },
- content = { contentPadding ->
- if (isConfigLoaded)
- AccountContent(this, contentPadding)
- else
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- CircularProgressIndicator()
- }
- }
- )
- }
-
- @Composable
- fun AccountContent(ctx: Context, contentPadding: PaddingValues) {
-
- oldNickname = acc.nickName.value
- newNickname = oldNickname
- oldDisplayname = acc.displayName
- newDisplayname = oldDisplayname
- oldAuthUser = acc.authUser
- newAuthUser = oldAuthUser
- if (BaresipService.aorPasswords[aor] == null && // check if OK
- acc.authPass != NO_AUTH_PASS) {
- oldAuthPass = acc.authPass
- newAuthPass = oldAuthPass
- }
- if (acc.outbound.isNotEmpty()) {
- oldOutbound1 = acc.outbound[0]
- newOutbound1 = oldOutbound1
- if (acc.outbound.size > 1) {
- oldOutbound2 = acc.outbound[1]
- newOutbound2 = oldOutbound2
- }
- }
- oldRegister = acc.regint > 0
- newRegister = oldRegister
- oldRegInt = acc.configuredRegInt.toString()
- newRegInt = oldRegInt
- oldMediaEnc = acc.mediaEnc
- newMediaEnc = oldMediaEnc
- oldMediaNat = acc.mediaNat
- newMediaNat = oldMediaNat
- showStun.value = oldMediaNat != ""
- oldStunServer = acc.stunServer
- newStunServer = oldStunServer
- oldStunUser = acc.stunUser
- newStunUser = oldStunUser
- oldStunPass = acc.stunPass
- newStunPass = oldStunPass
- oldRtcpMux = acc.rtcpMux
- newRtcpMux = oldRtcpMux
- old100Rel = acc.rel100Mode == Api.REL100_ENABLED
- new100Rel = old100Rel
- oldDtmfMode = acc.dtmfMode
- newDtmfMode = oldDtmfMode
- oldAnswerMode = acc.answerMode
- newAnswerMode = oldAnswerMode
- oldAutoRedirect = acc.autoRedirect
- newAutoRedirect = oldAutoRedirect
- oldVmUri = acc.vmUri
- newVmUri = oldVmUri
- oldCountryCode = acc.countryCode
- newCountryCode = oldCountryCode
- oldTelProvider = acc.telProvider
- newTelProvider = oldTelProvider
- newNumericKeypad = acc.numericKeypad
- oldDefaultAccount = UserAgent.findAorIndex(aor)!! == 0
- newDefaultAccount = oldDefaultAccount
-
- val scrollState = rememberScrollState()
-
- if (showAlert.value) {
- AlertDialog(
- showDialog = showAlert,
- title = alertTitle.value,
- message = alertMessage.value,
- positiveButtonText = stringResource(R.string.ok),
- )
- }
-
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(contentPadding)
- .padding(start = 16.dp, end = 4.dp, top = 8.dp, bottom = 16.dp)
- .verticalScrollbar(scrollState)
- .verticalScroll(state = scrollState),
- verticalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- AoR()
- Nickname()
- DisplayName()
- AuthUser()
- AuthPass()
- Outbound()
- Register()
- RegInt()
- AudioCodecs(ctx)
- MediaEnc()
- MediaNat()
- StunServer()
- StunUser()
- StunPass()
- RtcpMux()
- Rel100()
- Dtmf()
- Answer()
- Redirect()
- Voicemail()
- CountryCode()
- TelProvider()
- NumericKeypad()
- DefaultAccount()
- AskPassword(ctx)
- }
- }
-
- @Composable
- private fun AoR() {
- Row(
- Modifier.fillMaxWidth().padding(top=8.dp, end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- OutlinedTextField(
- value = acc.luri,
- enabled = false,
- onValueChange = {},
- modifier = Modifier.fillMaxWidth(),
- textStyle = TextStyle(
- fontSize = 18.sp,
- color = LocalCustomColors.current.itemText
- ),
- label = {
- LabelText(text = stringResource(R.string.sip_uri),
- fontWeight = FontWeight.Bold)
- }
- )
- }
- }
-
- @Composable
- private fun Nickname() {
- Row(
- Modifier.fillMaxWidth().padding(top=8.dp, end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var nickName by remember { mutableStateOf(oldNickname) }
- OutlinedTextField(
- value = nickName,
- placeholder = { Text(stringResource(R.string.nickname)) },
- onValueChange = {
- nickName = it
- newNickname = nickName
- },
- modifier = Modifier.fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.nickname)
- alertMessage.value = getString(R.string.account_nickname_help)
- showAlert.value = true },
- textStyle = TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.nickname)) },
- keyboardOptions = KeyboardOptions(
- capitalization = KeyboardCapitalization.Words,
- keyboardType = KeyboardType.Text),
- )
- }
- }
-
- @Composable
- private fun DisplayName() {
- Row(
- Modifier.fillMaxWidth().padding(top=8.dp, end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var displayName by remember { mutableStateOf(oldDisplayname) }
- OutlinedTextField(
- value = displayName,
- placeholder = { Text(stringResource(R.string.display_name)) },
- onValueChange = {
- displayName = it
- newDisplayname = displayName
- },
- modifier = Modifier.fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.display_name)
- alertMessage.value = getString(R.string.display_name_help)
- showAlert.value = true },
- textStyle = TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.display_name)) },
- keyboardOptions = KeyboardOptions(
- capitalization = KeyboardCapitalization.Sentences,
- keyboardType = KeyboardType.Text),
- )
- }
- }
-
- @Composable
- private fun AuthUser() {
- Row(
- Modifier.fillMaxWidth().padding(top=8.dp, end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var authUser by remember { mutableStateOf(oldAuthUser) }
- OutlinedTextField(
- value = authUser,
- placeholder = { Text(stringResource(R.string.authentication_username)) },
- onValueChange = {
- authUser = it
- newAuthUser = authUser
- },
- modifier = Modifier.fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.authentication_username)
- alertMessage.value = getString(R.string.authentication_username_help)
- showAlert.value = true },
- textStyle = TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.authentication_username)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- private fun AuthPass() {
- val showPassword = remember { mutableStateOf(false) }
- Row(
- Modifier.fillMaxWidth().padding(top=8.dp, end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var authPass by remember { mutableStateOf(oldAuthPass) }
- OutlinedTextField(
- value = authPass,
- placeholder = { Text(stringResource(R.string.authentication_password)) },
- onValueChange = {
- authPass = it
- newAuthPass = authPass
- },
- singleLine = true,
- visualTransformation = if (showPassword.value)
- VisualTransformation.None
- else
- PasswordVisualTransformation(),
- trailingIcon = {
- IconButton(onClick = {
- showPassword.value = !showPassword.value
- }) {
- Icon(
- if (showPassword.value)
- ImageVector.vectorResource(R.drawable.visibility)
- else
- ImageVector.vectorResource(R.drawable.visibility_off),
- contentDescription = "Visibility",
- tint = LocalCustomColors.current.itemText
-
- )
- }
- },
- modifier = Modifier.fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.authentication_password)
- alertMessage.value = getString(R.string.authentication_password_help)
- showAlert.value = true },
- textStyle = TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.authentication_password)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- private fun Outbound() {
- Text(text = stringResource(R.string.outbound_proxies),
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp,
- modifier = Modifier.padding(top=8.dp)
- .clickable {
- alertTitle.value = getString(R.string.outbound_proxies)
- alertMessage.value = getString(R.string.outbound_proxies_help)
- showAlert.value = true
- }
- )
- Row(
- Modifier.fillMaxWidth().padding(top=8.dp, end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var outbound1 by remember { mutableStateOf(oldOutbound1) }
- OutlinedTextField(
- value = outbound1,
- placeholder = { Text(stringResource(R.string.sip_uri_of_proxy_server)) },
- onValueChange = {
- outbound1 = it
- newOutbound1 = outbound1
- },
- modifier = Modifier.fillMaxWidth(),
- singleLine = true,
- textStyle = TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.sip_uri_of_proxy_server)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- Row(
- Modifier.fillMaxWidth().padding(top=8.dp, end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var outbound2 by remember { mutableStateOf(oldOutbound2) }
- OutlinedTextField(
- value = outbound2,
- placeholder = { Text(stringResource(R.string.sip_uri_of_another_proxy_server)) },
- onValueChange = {
- outbound2 = it
- newOutbound2 = outbound2
- },
- modifier = Modifier.fillMaxWidth(),
- singleLine = true,
- textStyle = TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.sip_uri_of_another_proxy_server)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- fun Register() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.register),
- modifier = Modifier.weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.register)
- alertMessage.value = getString(R.string.register_help)
- showAlert.value = true
- },
- fontSize = 18.sp,
- color = LocalCustomColors.current.itemText)
- var register by remember { mutableStateOf(oldRegister) }
- Switch(
- checked = register,
- onCheckedChange = {
- register = it
- newRegister = register
- }
- )
- }
- }
-
- @Composable
- private fun RegInt() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var regInt by remember { mutableStateOf(oldRegInt) }
- OutlinedTextField(
- value = regInt,
- placeholder = { Text(stringResource(R.string.reg_int)) },
- onValueChange = {
- regInt = it
- newRegInt = regInt
- },
- modifier = Modifier.fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.reg_int)
- alertMessage.value = getString(R.string.reg_int_help)
- showAlert.value = true },
- textStyle = TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.reg_int)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
- )
- }
- }
-
- @Composable
- private fun AudioCodecs(ctx: Context) {
- Row(
- Modifier.fillMaxWidth().padding(top=12.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(
- text = stringResource(R.string.audio_codecs),
- modifier = Modifier.weight(1f)
- .clickable {
- val i = Intent(ctx, CodecsActivity::class.java)
- val b = Bundle()
- b.putString("aor", aor)
- b.putString("media", "audio")
- i.putExtras(b)
- startActivity(i)
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp,
- fontWeight = FontWeight. Bold
- )
- }
- }
-
- @Composable
- private fun MediaEnc() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.media_encryption),
- modifier = Modifier.weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.media_encryption)
- alertMessage.value = getString(R.string.media_encryption_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- val isDropDownExpanded = remember { mutableStateOf(false) }
- val mediaEnc = remember { mutableStateOf(oldMediaEnc) }
- Box {
- Row(
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.clickable {
- isDropDownExpanded.value = true
- }
- ) {
- Text(text = mediaEncMap[mediaEnc.value]!!,
- color = LocalCustomColors.current.itemText)
- CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
- tint = LocalCustomColors.current.itemText)
- }
- DropdownMenu(
- expanded = isDropDownExpanded.value,
- onDismissRequest = {
- isDropDownExpanded.value = false
- }) {
- var index = 0
- mediaEncMap.forEach {
- DropdownMenuItem(text = {
- Text(text = it.value,
- color = LocalCustomColors.current.itemText)
- },
- onClick = {
- isDropDownExpanded.value = false
- mediaEnc.value = it.key
- newMediaEnc = mediaEnc.value
- })
- if (index < 4)
- HorizontalDivider(
- thickness = 1.dp,
- color = LocalCustomColors.current.itemText
- )
- index++
- }
- }
- }
- }
- }
-
- @Composable
- private fun MediaNat() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.media_nat),
- modifier = Modifier.weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.media_nat)
- alertMessage.value = getString(R.string.media_nat_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- val isDropDownExpanded = remember { mutableStateOf(false) }
- val mediaNat = remember { mutableStateOf(oldMediaNat) }
- Box {
- Row(
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.clickable {
- isDropDownExpanded.value = true
- }
- ) {
- Text(text = mediaNatMap[mediaNat.value]!!,
- color = LocalCustomColors.current.itemText)
- CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
- tint = LocalCustomColors.current.itemText)
- }
- DropdownMenu(
- expanded = isDropDownExpanded.value,
- onDismissRequest = {
- isDropDownExpanded.value = false
- }) {
- var index = 0
- mediaNatMap.forEach {
- DropdownMenuItem(text = {
- Text(text = it.value)
- },
- onClick = {
- isDropDownExpanded.value = false
- mediaNat.value = it.key
- newMediaNat = mediaNat.value
- showStun.value = newMediaNat != ""
- })
- if (index < 3)
- HorizontalDivider(
- thickness = 1.dp,
- color = LocalCustomColors.current.itemText
- )
- index++
- }
- }
- }
- }
- }
-
- @Composable
- private fun StunServer() {
- if (showStun.value)
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var stunServer by remember { mutableStateOf(oldStunServer) }
- OutlinedTextField(
- value = stunServer,
- placeholder = { Text(stringResource(R.string.stun_server)) },
- onValueChange = {
- stunServer = it
- newStunServer = stunServer
- },
- modifier = Modifier.fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.stun_server)
- alertMessage.value = getString(R.string.stun_server_help)
- showAlert.value = true },
- textStyle = TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.stun_server)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- private fun StunUser() {
- if (showStun.value)
- Row(
- Modifier.fillMaxWidth().padding(top=8.dp, end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var stunUser by remember { mutableStateOf(oldStunUser) }
- OutlinedTextField(
- value = stunUser,
- placeholder = { Text(stringResource(R.string.stun_username)) },
- onValueChange = {
- stunUser = it
- newStunUser = stunUser
- },
- modifier = Modifier.fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.stun_username)
- alertMessage.value = getString(R.string.stun_username_help)
- showAlert.value = true },
- textStyle = TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.stun_username)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- private fun StunPass() {
- if (showStun.value) {
- val showPassword = remember { mutableStateOf(false) }
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var stunPass by remember { mutableStateOf(oldStunPass) }
- OutlinedTextField(
- value = stunPass,
- placeholder = { Text(stringResource(R.string.stun_password)) },
- onValueChange = {
- stunPass = it
- newStunPass = stunPass
- },
- singleLine = true,
- visualTransformation = if (showPassword.value)
- VisualTransformation.None
- else
- PasswordVisualTransformation(),
- trailingIcon = {
- val (icon, iconColor) = if (showPassword.value) {
- Pair(
- ImageVector.vectorResource(R.drawable.visibility),
- colorResource(id = R.color.colorAccent)
- )
- } else {
- Pair(
- ImageVector.vectorResource(R.drawable.visibility_off),
- colorResource(id = R.color.colorWhite)
- )
- }
- IconButton(onClick = { showPassword.value = !showPassword.value }) {
- Icon(
- icon,
- contentDescription = "Visibility",
- tint = iconColor
- )
- }
- },
- modifier = Modifier.fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.stun_password)
- alertMessage.value = getString(R.string.stun_password_help)
- showAlert.value = true
- },
- textStyle = TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText
- ),
- label = { LabelText(stringResource(R.string.stun_password)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
- }
-
- @Composable
- fun RtcpMux() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.rtcp_mux),
- modifier = Modifier.weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.rtcp_mux)
- alertMessage.value = getString(R.string.rtcp_mux_help)
- showAlert.value = true
- },
- fontSize = 18.sp,
- color = LocalCustomColors.current.itemText)
- var rtcpMux by remember { mutableStateOf(oldRtcpMux) }
- Switch(
- checked = rtcpMux,
- onCheckedChange = {
- rtcpMux = it
- newRtcpMux = rtcpMux
- }
- )
- }
- }
-
- @Composable
- fun Rel100() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.rel_100),
- modifier = Modifier.weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.rel_100)
- alertMessage.value = getString(R.string.rel_100_help)
- showAlert.value = true
- },
- fontSize = 18.sp,
- color = LocalCustomColors.current.itemText)
- var rel100 by remember { mutableStateOf(old100Rel) }
- Switch(
- checked = rel100,
- onCheckedChange = {
- rel100 = it
- new100Rel = rel100
- }
- )
- }
- }
-
- @Composable
- private fun Dtmf() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.dtmf_mode),
- modifier = Modifier.weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.dtmf_mode)
- alertMessage.value = getString(R.string.dtmf_mode_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- val isDropDownExpanded = remember {
- mutableStateOf(false)
- }
- val dtmfMode = remember { mutableIntStateOf(oldDtmfMode) }
- Box {
- Row(
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.clickable {
- isDropDownExpanded.value = true
- }
- ) {
- Text(text = dtmfModeMap[dtmfMode.intValue]!!,
- color = LocalCustomColors.current.itemText)
- CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
- tint = LocalCustomColors.current.itemText)
- }
- DropdownMenu(
- expanded = isDropDownExpanded.value,
- onDismissRequest = {
- isDropDownExpanded.value = false
- }) {
- var index = 0
- dtmfModeMap.forEach {
- DropdownMenuItem(text = {
- Text(text = it.value)
- },
- onClick = {
- isDropDownExpanded.value = false
- dtmfMode.intValue = it.key
- newDtmfMode = dtmfMode.intValue
- })
- if (index < 2)
- HorizontalDivider(
- thickness = 1.dp,
- color = LocalCustomColors.current.itemText
- )
- index++
- }
- }
- }
- }
- }
-
- @Composable
- private fun Answer() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.answer_mode),
- modifier = Modifier.weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.answer_mode)
- alertMessage.value = getString(R.string.answer_mode_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- val isDropDownExpanded = remember {
- mutableStateOf(false)
- }
- val answerMode = remember { mutableIntStateOf(oldAnswerMode) }
- Box {
- Row(
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.clickable {
- isDropDownExpanded.value = true
- }
- ) {
- Text(text = answerModeMap[answerMode.intValue]!!,
- color = LocalCustomColors.current.itemText)
- CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
- tint = LocalCustomColors.current.itemText)
- }
- DropdownMenu(
- expanded = isDropDownExpanded.value,
- onDismissRequest = {
- isDropDownExpanded.value = false
- }) {
- var index = 0
- answerModeMap.forEach {
- DropdownMenuItem(text = { Text(text = it.value) },
- onClick = {
- isDropDownExpanded.value = false
- answerMode.intValue = it.key
- newAnswerMode = answerMode.intValue
- })
- if (index < 1)
- HorizontalDivider(
- thickness = 1.dp,
- color = LocalCustomColors.current.itemText
- )
- index++
- }
- }
- }
- }
- }
-
- @Composable
- private fun Redirect() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.redirect_mode),
- modifier = Modifier.weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.redirect_mode)
- alertMessage.value = getString(R.string.redirect_mode_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- val isDropDownExpanded = remember {
- mutableStateOf(false)
- }
- val autoRedirect = remember { mutableStateOf(oldAutoRedirect) }
- Box {
- Row(
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.clickable {
- isDropDownExpanded.value = true
- }
- ) {
- Text(text = redirectModeMap[autoRedirect.value]!!,
- color = LocalCustomColors.current.itemText)
- CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
- tint = LocalCustomColors.current.itemText)
- }
- DropdownMenu(
- expanded = isDropDownExpanded.value,
- onDismissRequest = {
- isDropDownExpanded.value = false
- }) {
- var index = 0
- redirectModeMap.forEach {
- DropdownMenuItem(text = {
- Text(text = it.value)
- },
- onClick = {
- isDropDownExpanded.value = false
- autoRedirect.value = it.key
- newAutoRedirect = autoRedirect.value
- })
- if (index < 1)
- HorizontalDivider(
- thickness = 1.dp,
- color = LocalCustomColors.current.itemText
- )
- index++
- }
- }
- }
- }
- }
-
- @Composable
- private fun Voicemail() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var vmUri by remember { mutableStateOf(oldVmUri) }
- OutlinedTextField(
- value = vmUri,
- placeholder = { Text(stringResource(R.string.voicemail_uri)) },
- onValueChange = {
- vmUri = it
- newVmUri = vmUri
- },
- modifier = Modifier.fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.voicemail_uri)
- alertMessage.value = getString(R.string.voicemain_uri_help)
- showAlert.value = true },
- textStyle = TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.voicemail_uri)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- private fun CountryCode() {
- Row(
- Modifier.fillMaxWidth().padding(top=8.dp, end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var countryCode by remember { mutableStateOf(oldCountryCode) }
- OutlinedTextField(
- value = countryCode,
- placeholder = { Text(stringResource(R.string.country_code)) },
- onValueChange = {
- countryCode = it
- newCountryCode = countryCode
- },
- modifier = Modifier.fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.country_code)
- alertMessage.value = getString(R.string.country_code_help)
- showAlert.value = true },
- textStyle = TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.country_code)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- private fun TelProvider() {
- Row(
- Modifier.fillMaxWidth().padding(top=8.dp, end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var telProvider by remember { mutableStateOf(oldTelProvider) }
- OutlinedTextField(
- value = telProvider,
- placeholder = { Text(stringResource(R.string.telephony_provider)) },
- onValueChange = {
- telProvider = it
- newTelProvider = telProvider
- },
- modifier = Modifier.fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.telephony_provider)
- alertMessage.value = getString(R.string.telephony_provider_help)
- showAlert.value = true },
- textStyle = TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.telephony_provider)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- fun NumericKeypad() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.numeric_keypad),
- modifier = Modifier.weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.numeric_keypad)
- alertMessage.value = getString(R.string.numeric_keypad_help)
- showAlert.value = true
- },
- fontSize = 18.sp,
- color = LocalCustomColors.current.itemText)
- var numericKeypad by remember { mutableStateOf(acc.numericKeypad) }
- Switch(
- checked = numericKeypad,
- onCheckedChange = {
- numericKeypad = it
- newNumericKeypad = numericKeypad
- }
- )
- }
- }
-
- @Composable
- fun DefaultAccount() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.default_account),
- modifier = Modifier.weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.default_account)
- alertMessage.value = getString(R.string.default_account_help)
- showAlert.value = true },
- fontSize = 18.sp,
- color = LocalCustomColors.current.itemText)
- var defaultAccount by remember { mutableStateOf(oldDefaultAccount) }
- Switch(
- checked = defaultAccount,
- onCheckedChange = {
- defaultAccount = it
- newDefaultAccount = defaultAccount
- }
- )
- }
- }
-
- @Composable
- fun AskPassword(ctx: Context) {
- if (showPasswordDialog.value)
- CustomElements.PasswordDialog(
- ctx = ctx,
- showPasswordDialog = showPasswordDialog,
- password = password,
- keyboardController = keyboardController,
- title = stringResource(R.string.authentication_password),
- okAction = {
- if (password.value != "") {
- BaresipService.aorPasswords[acc.aor] = password.value
- Api.account_set_auth_pass(acc.accp, password.value)
- password.value = ""
- reRegister = true
- finishActivity()
- }
- },
- cancelAction = {
- reRegister = true
- finishActivity()
- }
- )
- }
-
- private fun updateAccount() {
-
- if (BaresipService.activities.indexOf("account,$aor") == -1)
- return
-
- val nn = newNickname.trim()
- if (nn != oldNickname) {
- if (Account.checkDisplayName(nn)) {
- if (nn == "" || Account.uniqueNickName(nn)) {
- acc.nickName.value = nn
- Log.d(TAG, "New nickname is ${acc.nickName.value}")
- }
- else {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.non_unique_account_nickname), nn)
- showAlert.value = true
- return
- }
- }
- else {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_account_nickname), nn)
- showAlert.value = true
- return
- }
- }
-
- val dn = newDisplayname.trim()
- if (dn != acc.displayName) {
- if (Account.checkDisplayName(dn)) {
- if (Api.account_set_display_name(acc.accp, dn) == 0) {
- acc.displayName = Api.account_display_name(acc.accp)
- Log.d(TAG, "New display name is ${acc.displayName}")
- } else {
- Log.e(TAG, "Setting of display name failed")
- }
- }
- else {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_display_name), dn)
- showAlert.value = true
- return
- }
- }
-
- val au = newAuthUser.trim()
- if (au != oldAuthUser) {
- if (Account.checkAuthUser(au)) {
- if (Api.account_set_auth_user(acc.accp, au) == 0) {
- acc.authUser = Api.account_auth_user(acc.accp)
- Log.d(TAG, "New auth user is ${acc.authUser}")
- if (acc.regint > 0)
- reRegister = true
- }
- else {
- Log.e(TAG, "Setting of auth user failed")
- }
- }
- else {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_authentication_username), au)
- showAlert.value = true
- return
- }
- }
-
- val ap = newAuthPass.trim()
- if (ap != "") {
- if (ap != oldAuthPass) {
- if (Account.checkAuthPass(ap)) {
- if (Api.account_set_auth_pass(acc.accp, ap) == 0) {
- acc.authPass = Api.account_auth_pass(acc.accp)
- if (acc.regint > 0)
- reRegister = true
- }
- else
- Log.e(TAG, "Setting of auth pass failed")
- BaresipService.aorPasswords.remove(acc.aor)
- }
- else {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_authentication_password), ap)
- showAlert.value = true
- return
- }
- }
- else
- BaresipService.aorPasswords.remove(acc.aor)
- }
- else { // ap == ""
- if (acc.authPass != NO_AUTH_PASS &&
- acc.authPass != BaresipService.aorPasswords[acc.aor])
- if (Api.account_set_auth_pass(acc.accp, "") == 0) {
- acc.authPass = NO_AUTH_PASS
- BaresipService.aorPasswords[acc.aor] = NO_AUTH_PASS
- }
- }
-
- val ob = ArrayList()
- var ob1 = newOutbound1.trim().replace(" ", "")
- if (ob1 != "") {
- if (!ob1.startsWith("sip:"))
- ob1 = "sip:$ob1"
- if (checkOutboundUri(ob1)) {
- ob.add(ob1)
- }
- else {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_proxy_server_uri), ob1)
- showAlert.value = true
- return
- }
- }
- var ob2 = newOutbound2.trim().replace(" ", "")
- if (ob2 != "") {
- if (!ob2.startsWith("sip:"))
- ob2 = "sip:$ob2"
- if (checkOutboundUri(ob2))
- ob.add(ob2)
- else {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_proxy_server_uri), ob2)
- showAlert.value = true
- return
- }
- }
- if (ob != acc.outbound) {
- for (i in 0..1) {
- val uri = if (ob.size > i)
- ob[i]
- else
- ""
- if (Api.account_set_outbound(acc.accp, uri, i) != 0)
- Log.e(TAG, "Setting of outbound proxy $i uri '$uri' failed")
- }
- Log.d(TAG, "New outbound proxies are $ob")
- acc.outbound = ob
- if (ob.isEmpty())
- Api.account_set_sipnat(acc.accp, "")
- else
- Api.account_set_sipnat(acc.accp, "outbound")
- if (acc.regint > 0)
- reRegister = true
- }
-
- val regInt = newRegInt.trim().toInt()
- if (regInt < 60 || regInt > 3600) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_reg_int), "$regInt")
- showAlert.value = true
- return
- }
- val reReg = (newRegister != acc.regint > 0) ||
- (newRegister && regInt != acc.configuredRegInt)
- if (reReg) {
- if (Api.account_set_regint(acc.accp,
- if (newRegister) regInt else 0) != 0) {
- Log.e(TAG, "Setting of regint failed")
- } else {
- acc.regint = Api.account_regint(acc.accp)
- acc.configuredRegInt = regInt
- Log.d(TAG, "New regint is ${acc.regint}")
- reRegister = true
- }
- } else {
- if (regInt != acc.configuredRegInt) {
- acc.configuredRegInt = regInt
- }
- }
-
- if (newMediaEnc != acc.mediaEnc) {
- if (Api.account_set_mediaenc(acc.accp, newMediaEnc) == 0) {
- acc.mediaEnc = Api.account_mediaenc(acc.accp)
- Log.d(TAG, "New mediaenc is ${acc.mediaEnc}")
- } else {
- Log.e(TAG, "Setting of mediaenc $newMediaEnc failed")
- }
- }
-
- if (newMediaNat != acc.mediaNat) {
- if (Api.account_set_medianat(acc.accp, newMediaNat) == 0) {
- acc.mediaNat = Api.account_medianat(acc.accp)
- Log.d(TAG, "New medianat is ${acc.mediaNat}")
- } else {
- Log.e(TAG, "Setting of medianat $newMediaNat failed")
- }
- }
-
- newStunServer = newStunServer.trim()
-
- if (newMediaNat != "") {
- if (((newMediaNat == "stun") || (newMediaNat == "ice")) && (newStunServer == ""))
- newStunServer = resources.getString(R.string.stun_server_default)
- if (!Utils.checkStunUri(newStunServer) ||
- (newMediaNat == "turn" &&
- newStunServer.substringBefore(":") !in setOf("turn", "turns"))) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_stun_server), newStunServer)
- showAlert.value = true
- return
- }
- }
-
- if (acc.stunServer != newStunServer) {
- if (Api.account_set_stun_uri(acc.accp, newStunServer) == 0) {
- acc.stunServer = Api.account_stun_uri(acc.accp)
- Log.d(TAG, "New STUN/TURN server URI is '${acc.stunServer}'")
- } else {
- Log.e(TAG, "Setting of STUN/TURN URI server failed")
- }
- }
-
- newStunUser = newStunUser.trim()
- if (acc.stunUser != newStunUser) {
- if (Account.checkAuthUser(newStunUser)) {
- if (Api.account_set_stun_user(acc.accp, newStunUser) == 0) {
- acc.stunUser = Api.account_stun_user(acc.accp)
- Log.d(TAG, "New STUN/TURN user is ${acc.stunUser}")
- }
- else
- Log.e(TAG, "Setting of STUN/TURN user failed")
- }
- else {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_stun_username), newStunUser)
- showAlert.value = true
- return
- }
- }
-
- val newStunPass = newStunPass.trim()
- if (acc.stunPass != newStunPass) {
- if (newStunPass.isEmpty() || Account.checkAuthPass(newStunPass)) {
- if (Api.account_set_stun_pass(acc.accp, newStunPass) == 0)
- acc.stunPass = Api.account_stun_pass(acc.accp)
- else
- Log.e(TAG, "Setting of stun pass failed")
- }
- else {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_stun_password), newStunPass)
- showAlert.value = true
- return
- }
- }
-
- if (newRtcpMux != acc.rtcpMux)
- if (Api.account_set_rtcp_mux(acc.accp, newRtcpMux) == 0) {
- acc.rtcpMux = Api.account_rtcp_mux(acc.accp)
- Log.d(TAG, "New rtcpMux is ${acc.rtcpMux}")
- } else {
- Log.e(TAG, "Setting of account_rtc_mux $newRtcpMux failed")
- }
-
- if (new100Rel != (acc.rel100Mode == Api.REL100_ENABLED)) {
- val mode = if (new100Rel) Api.REL100_ENABLED else Api.REL100_DISABLED
- if (Api.account_set_rel100_mode(acc.accp, mode) == 0) {
- acc.rel100Mode = Api.account_rel100_mode(acc.accp)
- Api.ua_update_account(ua.uap)
- Log.d(TAG, "New rel100Mode is ${acc.rel100Mode}")
- } else {
- Log.e(TAG, "Setting of account_rel100Mode failed")
- }
- }
-
- if (newDtmfMode != acc.dtmfMode) {
- if (Api.account_set_dtmfmode(acc.accp, newDtmfMode) == 0) {
- acc.dtmfMode = Api.account_dtmfmode(acc.accp)
- Log.d(TAG, "New dtmfmode is ${acc.dtmfMode}")
- } else {
- Log.e(TAG, "Setting of dtmfmode $newDtmfMode failed")
- }
- }
-
- if (newAnswerMode != acc.answerMode) {
- if (Api.account_set_answermode(acc.accp, newAnswerMode) == 0) {
- acc.answerMode = Api.account_answermode(acc.accp)
- Log.d(TAG, "New answermode is ${acc.answerMode}")
- } else {
- Log.e(TAG, "Setting of answermode $newAnswerMode failed")
- }
- }
-
- if (newAutoRedirect != acc.autoRedirect) {
- Api.account_set_sip_autoredirect(acc.accp, newAutoRedirect)
- acc.autoRedirect = newAutoRedirect
- Log.d(TAG, "New autoRedirect is ${acc.autoRedirect}")
- }
-
- newVmUri = newVmUri.trim()
- if (newVmUri != acc.vmUri) {
- if (newVmUri != "") {
- if (!newVmUri.startsWith("sip:")) newVmUri = "sip:$newVmUri"
- if (!newVmUri.contains("@")) newVmUri = "$newVmUri@${acc.host()}"
- if (!Utils.checkUri(newVmUri)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_sip_or_tel_uri), newVmUri)
- showAlert.value = true
- return
- }
- Api.account_set_mwi(acc.accp, true)
- }
- else
- Api.account_set_mwi(acc.accp, false)
- acc.vmUri = newVmUri
- Log.d(TAG, "New voicemail URI is ${acc.vmUri}")
- }
-
- newCountryCode = newCountryCode.trim()
- if (newCountryCode != acc.countryCode) {
- if (newCountryCode != "" && !Utils.checkCountryCode(newCountryCode)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_country_code), newCountryCode)
- showAlert.value = true
- return
- }
- acc.countryCode = newCountryCode
- Log.d(TAG, "New country code is ${acc.countryCode}")
- }
-
- val hostPart = newTelProvider.trim()
- if (hostPart != acc.telProvider) {
- if (hostPart != "" && !Utils.checkHostPortParams(hostPart)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_sip_uri_hostpart), hostPart)
- showAlert.value = true
- return
- }
- acc.telProvider = hostPart
- Log.d(TAG, "New tel provider is ${acc.telProvider}")
- }
-
- if (newNumericKeypad != acc.numericKeypad) {
- acc.numericKeypad = newNumericKeypad
- Log.d(TAG, "New numericKeyboard is ${acc.numericKeypad}")
- }
-
- val uaIndex = UserAgent.findAorIndex(aor)!!
- if (newDefaultAccount && (uaIndex > 0)) {
- val updatedUas = uas.value.toMutableList()
- updatedUas.add(0, uas.value[uaIndex])
- updatedUas.removeAt(uaIndex + 1)
- uas.value = updatedUas.toList()
- }
-
- AccountsActivity.saveAccounts()
-
- if (acc.authUser != "" && BaresipService.aorPasswords[aor] == NO_AUTH_PASS)
- showPasswordDialog.value = true
- else
- finishActivity()
- }
-
- private fun finishActivity() {
- if (reRegister) {
- ua.status = R.drawable.circle_yellow
- if (acc.regint == 0)
- Api.ua_unregister(ua.uap)
- else
- Api.ua_register(ua.uap)
- }
- BaresipService.activities.remove("account,$aor")
- returnResult(RESULT_OK)
- }
-
- private fun goBack() {
- BaresipService.activities.remove("account,$aor")
- returnResult(RESULT_CANCELED)
- }
-
- override fun onPause() {
- MainActivity.activityAor = aor
- super.onPause()
- }
-
- override fun onDestroy() {
- if (Build.VERSION.SDK_INT >= 33) {
- if (backInvokedCallback != null)
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
- }
- else
- onBackPressedCallback.remove()
- super.onDestroy()
- }
-
- private fun returnResult(code: Int) {
- val i = Intent()
- if (code == RESULT_OK)
- i.putExtra("aor", aor)
- setResult(code, i)
- finish()
- }
-
- private fun checkOutboundUri(uri: String): Boolean {
- if (!uri.startsWith("sip:")) return false
- return Utils.checkHostPortParams(uri.substring(4))
- }
-
-
- private fun initAccountFromConfig(acc: Account, onConfigLoaded: () -> Unit) {
- val scope = CoroutineScope(Job() + Dispatchers.Main)
- scope.launch(Dispatchers.IO) {
- val url = "https://${Utils.uriHostPart(acc.aor)}/baresip/account_config.xml"
- val config = try {
- URL(url).readText()
- } catch (e: java.lang.Exception) {
- Log.d(TAG, "Failed to get account configuration from network: ${e.message}")
- null
- }
- if (config != null) {
- Log.d(TAG, "Got account config '$config'")
- val parserFactory: XmlPullParserFactory = XmlPullParserFactory.newInstance()
- val parser: XmlPullParser = parserFactory.newPullParser()
- parser.setInput(StringReader(config))
- var tag: String?
- var text = ""
- var event = parser.eventType
- val audioCodecs = ArrayList(Api.audio_codecs().split(","))
- val videoCodecs = ArrayList(Api.video_codecs().split(","))
-
- while (event != XmlPullParser.END_DOCUMENT) {
- tag = parser.name
- when (event) {
- XmlPullParser.TEXT ->
- text = parser.text
-
- XmlPullParser.START_TAG -> {
- if (tag == "audio-codecs")
- acc.audioCodec.clear()
- if (tag == "video-codecs")
- acc.videoCodec.clear()
- }
-
- XmlPullParser.END_TAG ->
- when (tag) {
- "outbound-proxy-1" ->
- if (text.isNotEmpty())
- acc.outbound.add(text)
-
- "outbound-proxy-2" ->
- if (text.isNotEmpty())
- acc.outbound.add(text)
-
- "registration-interval" ->
- acc.configuredRegInt = text.toInt()
-
- "register" ->
- acc.regint = if (text == "yes") acc.configuredRegInt else 0
-
- "audio-codec" ->
- if (text in audioCodecs)
- acc.audioCodec.add(text)
-
- "video-codec" ->
- if (text in videoCodecs)
- acc.videoCodec.add(text)
-
- "media-encoding" -> {
- val enc = text.lowercase(Locale.ROOT)
- if (enc in mediaEncMap.keys && enc.isNotEmpty())
- acc.mediaEnc = enc
- }
-
- "media-nat" -> {
- val nat = text.lowercase(Locale.ROOT)
- if (nat in mediaNatMap.keys && nat.isNotEmpty())
- acc.mediaNat = nat
- }
-
- "stun-turn-server" ->
- if (text.isNotEmpty())
- acc.stunServer = text
-
- "rtcp-mux" ->
- acc.rtcpMux = text == "yes"
-
- "100rel-mode" ->
- acc.rel100Mode = if (text == "yes")
- Api.REL100_ENABLED
- else
- Api.REL100_DISABLED
-
- "dtmf-mode" ->
- if (text in arrayOf("rtp-event", "sip-info", "auto"))
- acc.dtmfMode = when (text) {
- "rtp-event" -> Api.DTMFMODE_RTP_EVENT
- "sip-info" -> Api.DTMFMODE_SIP_INFO
- else -> Api.DTMFMODE_AUTO
- }
-
- "answer-mode" ->
- if (text in arrayOf("manual", "auto"))
- acc.answerMode = if (text == "manual")
- Api.ANSWERMODE_MANUAL
- else
- Api.ANSWERMODE_AUTO
-
- "redirect-mode" ->
- acc.autoRedirect = text == "yes"
-
- "voicemail-uri" ->
- if (text.isNotEmpty())
- acc.vmUri = text
-
- "country-code" ->
- acc.countryCode = text
-
- "tel-provider" ->
- acc.telProvider = text
- }
- }
- event = parser.next()
- }
- }
- onConfigLoaded()
- }
- }
-
-}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt
new file mode 100644
index 00000000..5e879f71
--- /dev/null
+++ b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt
@@ -0,0 +1,1885 @@
+package com.tutpro.baresip
+
+import android.content.Context
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.platform.SoftwareKeyboardController
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import com.tutpro.baresip.CustomElements.AlertDialog
+import com.tutpro.baresip.CustomElements.LabelText
+import com.tutpro.baresip.CustomElements.verticalScrollbar
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserFactory
+import java.io.StringReader
+import java.net.URL
+import java.util.Locale
+
+fun NavGraphBuilder.accountScreenRoute(navController: NavController) {
+ composable(
+ route = "account/{aor}/{kind}",
+ arguments = listOf(
+ navArgument("aor") { type = NavType.StringType },
+ navArgument("kind") { type = NavType.StringType }
+ )
+ ) { backStackEntry ->
+ val ctx = LocalContext.current
+ val aor = backStackEntry.arguments?.getString("aor")!!
+ val kind = backStackEntry.arguments?.getString("kind")!!
+ val ua = UserAgent.ofAor(aor)!!
+ AccountScreen(
+ navController = navController,
+ onBack = { navController.popBackStack() },
+ checkOnClick = {
+ val ok = checkOnClick(ctx, ua)
+ if (ok) {
+ if (reRegister) ua.reRegister()
+ navController.popBackStack()
+ }
+ },
+ aor = aor,
+ kind = kind
+ )
+ }
+}
+
+private var keyboardController: SoftwareKeyboardController? = null
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun AccountScreen(
+ navController: NavController,
+ onBack: () -> Unit,
+ checkOnClick: () -> Unit,
+ aor: String,
+ kind: String
+) {
+ val ua = UserAgent.ofAor(aor)!!
+ val acc = ua.account
+
+ var isConfigLoaded by remember { mutableStateOf(false) }
+
+ LaunchedEffect(kind, acc) {
+ if (kind == "new")
+ initAccountFromConfig(acc) { isConfigLoaded = true }
+ else
+ isConfigLoaded = true
+ }
+
+ val title = if (acc.nickName.value != "")
+ acc.nickName.value
+ else
+ acc.aor.substringAfter(":")
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize().imePadding(),
+ containerColor = LocalCustomColors.current.background,
+ topBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(
+ top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
+ )
+ ) {
+ TopAppBar(
+ title = {
+ Text(
+ text = title,
+ color = LocalCustomColors.current.light,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = null,
+ tint = LocalCustomColors.current.light
+ )
+ }
+ },
+ windowInsets = WindowInsets(0, 0, 0, 0),
+ actions = {
+ IconButton(onClick = checkOnClick) {
+ Icon(
+ imageVector = Icons.Filled.Check,
+ tint = LocalCustomColors.current.light,
+ contentDescription = "Check"
+ )
+ }
+ },
+ colors = TopAppBarDefaults.mediumTopAppBarColors(
+ containerColor = LocalCustomColors.current.primary
+ )
+ )
+ }
+ }
+ ) { contentPadding ->
+ if (isConfigLoaded)
+ AccountContent(navController, contentPadding, ua)
+ else
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator()
+ }
+ }
+}
+
+private val password = mutableStateOf("")
+private val showPasswordDialog = mutableStateOf(false)
+
+private val alertTitle = mutableStateOf("")
+private val alertMessage = mutableStateOf("")
+private val showAlert = mutableStateOf(false)
+
+private var oldNickname = ""
+private var newNickname = ""
+private var oldDisplayname = ""
+private var newDisplayname = ""
+private var oldAuthUser = ""
+private var newAuthUser = ""
+private var oldAuthPass = ""
+private var newAuthPass = ""
+private val oldOutbound1 = mutableStateOf("")
+private val oldOutbound2 = mutableStateOf("")
+private var newOutbound1 = ""
+private var newOutbound2 = ""
+private var oldRegister = false
+private var newRegister = false
+private var oldRegInt = ""
+private var newRegInt = ""
+private var oldMediaEnc = ""
+private var newMediaEnc = ""
+private var oldMediaNat = ""
+private var newMediaNat = ""
+private var oldStunServer = ""
+private var newStunServer = ""
+private var oldStunUser = ""
+private var newStunUser = ""
+private var oldStunPass = ""
+private var newStunPass = ""
+private var oldRtcpMux = false
+private var newRtcpMux = false
+private var old100Rel = false
+private var new100Rel = false
+private var oldDtmfMode = 0
+private var newDtmfMode = 0
+private var oldAnswerMode = 0
+private var newAnswerMode = 0
+private var oldAutoRedirect = false
+private var newAutoRedirect = false
+private var oldVmUri = ""
+private var newVmUri = ""
+private var oldCountryCode = ""
+private var newCountryCode = ""
+private var oldTelProvider = ""
+private var newTelProvider = ""
+private var oldDefaultAccount = false
+private var newDefaultAccount = false
+private var newNumericKeypad = false
+
+private var reRegister = false
+
+@Composable
+private fun AccountContent(
+ navController: NavController,
+ contentPadding: PaddingValues,
+ ua: UserAgent
+) {
+ val ctx = LocalContext.current
+ val acc = ua.account
+ val aor = acc.aor
+
+ var mediaNatState by remember { mutableStateOf(oldMediaNat) }
+ val showStun by remember { derivedStateOf { mediaNatState != "" } }
+
+ LaunchedEffect(acc) {
+ if (acc.outbound.isNotEmpty()) {
+ oldOutbound1.value = acc.outbound[0]
+ newOutbound1 = oldOutbound1.value
+ if (acc.outbound.size > 1) {
+ oldOutbound2.value = acc.outbound[1]
+ newOutbound2 = oldOutbound2.value
+ } else {
+ oldOutbound2.value = ""
+ newOutbound2 = ""
+ }
+ } else {
+ oldOutbound1.value = ""
+ newOutbound1 = ""
+ oldOutbound2.value = ""
+ newOutbound2 = ""
+ }
+ oldMediaNat = acc.mediaNat
+ newMediaNat = oldMediaNat
+ mediaNatState = oldMediaNat
+ }
+
+ if (showAlert.value) {
+ AlertDialog(
+ showDialog = showAlert,
+ title = alertTitle.value,
+ message = alertMessage.value,
+ positiveButtonText = stringResource(R.string.ok),
+ )
+ }
+
+ oldNickname = acc.nickName.value
+ newNickname = oldNickname
+ oldDisplayname = acc.displayName
+ newDisplayname = oldDisplayname
+ oldAuthUser = acc.authUser
+ newAuthUser = oldAuthUser
+ if (BaresipService.aorPasswords[aor] == null && acc.authPass != NO_AUTH_PASS) {
+ oldAuthPass = acc.authPass
+ newAuthPass = oldAuthPass
+ }
+ else {
+ oldAuthPass = ""
+ newAuthPass = ""
+ }
+ oldRegister = acc.regint > 0
+ newRegister = oldRegister
+ oldRegInt = acc.configuredRegInt.toString()
+ newRegInt = oldRegInt
+ oldMediaEnc = acc.mediaEnc
+ newMediaEnc = oldMediaEnc
+ oldStunServer = acc.stunServer
+ newStunServer = oldStunServer
+ oldStunUser = acc.stunUser
+ newStunUser = oldStunUser
+ oldStunPass = acc.stunPass
+ newStunPass = oldStunPass
+ oldRtcpMux = acc.rtcpMux
+ newRtcpMux = oldRtcpMux
+ old100Rel = acc.rel100Mode == Api.REL100_ENABLED
+ new100Rel = old100Rel
+ oldDtmfMode = acc.dtmfMode
+ newDtmfMode = oldDtmfMode
+ oldAnswerMode = acc.answerMode
+ newAnswerMode = oldAnswerMode
+ oldAutoRedirect = acc.autoRedirect
+ newAutoRedirect = oldAutoRedirect
+ oldVmUri = acc.vmUri
+ newVmUri = oldVmUri
+ oldCountryCode = acc.countryCode
+ newCountryCode = oldCountryCode
+ oldTelProvider = acc.telProvider
+ newTelProvider = oldTelProvider
+ newNumericKeypad = acc.numericKeypad
+ oldDefaultAccount = UserAgent.findAorIndex(aor)!! == 0
+ newDefaultAccount = oldDefaultAccount
+
+ keyboardController = LocalSoftwareKeyboardController.current
+
+ val scrollState = rememberScrollState()
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(contentPadding)
+ .padding(start = 16.dp, end = 4.dp, top = 8.dp, bottom = 16.dp)
+ .verticalScrollbar(scrollState)
+ .verticalScroll(state = scrollState),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ AoR(acc)
+ Nickname()
+ DisplayName()
+ AuthUser()
+ AuthPass()
+ if (showPasswordDialog.value)
+ AskPassword(ctx, navController, ua)
+ Outbound()
+ Register()
+ RegInt()
+ AudioCodecs(navController, aor)
+ MediaEnc()
+ MediaNat(
+ currentMediaNat = mediaNatState,
+ onMediaNatSelected = { selectedMediaNat ->
+ mediaNatState = selectedMediaNat
+ newMediaNat = selectedMediaNat
+ }
+ )
+ if (showStun) {
+ StunServer()
+ StunUser()
+ StunPass()
+ }
+ RtcpMux()
+ Rel100()
+ Dtmf()
+ Answer()
+ Redirect()
+ Voicemail()
+ CountryCode()
+ TelProvider()
+ NumericKeypad(acc)
+ DefaultAccount()
+ }
+}
+
+@Composable
+private fun AoR(acc: Account) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ OutlinedTextField(
+ value = acc.luri,
+ enabled = false,
+ onValueChange = {},
+ modifier = Modifier.fillMaxWidth(),
+ textStyle = TextStyle(
+ fontSize = 18.sp,
+ color = LocalCustomColors.current.itemText
+ ),
+ label = {
+ LabelText(text = stringResource(R.string.sip_uri),
+ fontWeight = FontWeight.Bold)
+ }
+ )
+ }
+}
+
+@Composable
+private fun Nickname() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ var nickName by remember { mutableStateOf(oldNickname) }
+ OutlinedTextField(
+ value = nickName,
+ placeholder = { Text(stringResource(R.string.nickname)) },
+ onValueChange = {
+ nickName = it
+ newNickname = nickName
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.nickname)
+ alertMessage.value = ctx.getString(R.string.account_nickname_help)
+ showAlert.value = true
+ },
+ textStyle = TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.nickname)) },
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.Words,
+ keyboardType = KeyboardType.Text),
+ )
+ }
+}
+
+@Composable
+private fun DisplayName() {
+ val ctx = LocalContext.current
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ var displayName by remember { mutableStateOf(oldDisplayname) }
+ OutlinedTextField(
+ value = displayName,
+ placeholder = { Text(stringResource(R.string.display_name)) },
+ onValueChange = {
+ displayName = it
+ newDisplayname = displayName
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.display_name)
+ alertMessage.value = ctx.getString(R.string.display_name_help)
+ showAlert.value = true
+ },
+ textStyle = TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.display_name)) },
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.Sentences,
+ keyboardType = KeyboardType.Text),
+ )
+ }
+}
+
+@Composable
+private fun AuthUser() {
+ val ctx = LocalContext.current
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ var authUser by remember { mutableStateOf(oldAuthUser) }
+ OutlinedTextField(
+ value = authUser,
+ placeholder = { Text(stringResource(R.string.authentication_username)) },
+ onValueChange = {
+ authUser = it
+ newAuthUser = authUser
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.authentication_username)
+ alertMessage.value = ctx.getString(R.string.authentication_username_help)
+ showAlert.value = true
+ },
+ textStyle = TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.authentication_username)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun AuthPass() {
+ val ctx = LocalContext.current
+ val showPassword = remember { mutableStateOf(false) }
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ var authPass by remember { mutableStateOf(oldAuthPass) }
+ OutlinedTextField(
+ value = authPass,
+ placeholder = { Text(stringResource(R.string.authentication_password)) },
+ onValueChange = {
+ authPass = it
+ newAuthPass = authPass
+ },
+ singleLine = true,
+ visualTransformation = if (showPassword.value)
+ VisualTransformation.None
+ else
+ PasswordVisualTransformation(),
+ trailingIcon = {
+ IconButton(onClick = {
+ showPassword.value = !showPassword.value
+ }) {
+ Icon(
+ if (showPassword.value)
+ ImageVector.vectorResource(R.drawable.visibility)
+ else
+ ImageVector.vectorResource(R.drawable.visibility_off),
+ contentDescription = "Visibility",
+ tint = LocalCustomColors.current.itemText
+
+ )
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.authentication_password)
+ alertMessage.value = ctx.getString(R.string.authentication_password_help)
+ showAlert.value = true
+ },
+ textStyle = TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.authentication_password)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun AskPassword(ctx: Context, navController: NavController, ua: UserAgent) {
+ CustomElements.PasswordDialog(
+ ctx = ctx,
+ showPasswordDialog = showPasswordDialog,
+ password = password,
+ keyboardController = keyboardController,
+ title = stringResource(R.string.authentication_password),
+ okAction = {
+ if (password.value != "") {
+ BaresipService.aorPasswords[ua.account.aor] = password.value
+ Api.account_set_auth_pass(ua.account.accp, password.value)
+ password.value = ""
+ ua.reRegister()
+ navController.popBackStack()
+ }
+ },
+ cancelAction = {
+ ua.reRegister()
+ navController.popBackStack()
+ }
+ )
+}
+
+@Composable
+private fun Outbound() {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.outbound_proxies),
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp,
+ modifier = Modifier
+ .padding(top = 8.dp)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.outbound_proxies)
+ alertMessage.value = ctx.getString(R.string.outbound_proxies_help)
+ showAlert.value = true
+ }
+ )
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ var outbound1 by remember { mutableStateOf(oldOutbound1) }
+ OutlinedTextField(
+ value = outbound1.value,
+ placeholder = { Text(stringResource(R.string.sip_uri_of_proxy_server)) },
+ onValueChange = {
+ outbound1.value = it
+ newOutbound1 = outbound1.value
+ },
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ textStyle = TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.sip_uri_of_proxy_server)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ var outbound2 by remember { mutableStateOf(oldOutbound2) }
+ OutlinedTextField(
+ value = outbound2.value,
+ placeholder = { Text(stringResource(R.string.sip_uri_of_another_proxy_server)) },
+ onValueChange = {
+ outbound2.value = it
+ newOutbound2 = outbound2.value
+ },
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ textStyle = TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.sip_uri_of_another_proxy_server)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun Register() {
+ val ctx = LocalContext.current
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ Text(text = stringResource(R.string.register),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.register)
+ alertMessage.value = ctx.getString(R.string.register_help)
+ showAlert.value = true
+ },
+ fontSize = 18.sp,
+ color = LocalCustomColors.current.itemText)
+ var register by remember { mutableStateOf(oldRegister) }
+ Switch(
+ checked = register,
+ onCheckedChange = {
+ register = it
+ newRegister = register
+ }
+ )
+ }
+}
+
+@Composable
+private fun RegInt() {
+ val ctx = LocalContext.current
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ var regInt by remember { mutableStateOf(oldRegInt) }
+ OutlinedTextField(
+ value = regInt,
+ placeholder = { Text(stringResource(R.string.reg_int)) },
+ onValueChange = {
+ regInt = it
+ newRegInt = regInt
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.reg_int)
+ alertMessage.value = ctx.getString(R.string.reg_int_help)
+ showAlert.value = true
+ },
+ textStyle = TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.reg_int)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
+ )
+ }
+}
+
+@Composable
+private fun AudioCodecs(navController: NavController, aor: String) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ Text(
+ text = stringResource(R.string.audio_codecs),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ val route = "codecs/$aor/audio"
+ navController.navigate(route)
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp,
+ fontWeight = FontWeight. Bold
+ )
+ }
+}
+
+@Composable
+private fun MediaEnc() {
+ val ctx = LocalContext.current
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ Text(text = stringResource(R.string.media_encryption),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.media_encryption)
+ alertMessage.value = ctx.getString(R.string.media_encryption_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ val isDropDownExpanded = remember { mutableStateOf(false) }
+ val mediaEnc = remember { mutableStateOf(oldMediaEnc) }
+ Box {
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.clickable {
+ isDropDownExpanded.value = true
+ }
+ ) {
+ Text(text = mediaEncMap[mediaEnc.value]!!,
+ color = LocalCustomColors.current.itemText)
+ CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
+ tint = LocalCustomColors.current.itemText)
+ }
+ DropdownMenu(
+ expanded = isDropDownExpanded.value,
+ onDismissRequest = {
+ isDropDownExpanded.value = false
+ }) {
+ var index = 0
+ mediaEncMap.forEach {
+ DropdownMenuItem(text = {
+ Text(text = it.value,
+ color = LocalCustomColors.current.itemText)
+ },
+ onClick = {
+ isDropDownExpanded.value = false
+ mediaEnc.value = it.key
+ newMediaEnc = mediaEnc.value
+ })
+ if (index < 4)
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = LocalCustomColors.current.itemText
+ )
+ index++
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun MediaNat(currentMediaNat: String, onMediaNatSelected: (String) -> Unit) {
+ val ctx = LocalContext.current
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ Text(text = stringResource(R.string.media_nat),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.media_nat)
+ alertMessage.value = ctx.getString(R.string.media_nat_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ val isDropDownExpanded = remember { mutableStateOf(false) }
+ Box {
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.clickable {
+ isDropDownExpanded.value = true
+ }
+ ) {
+ Text(text = mediaNatMap[currentMediaNat]!!,
+ color = LocalCustomColors.current.itemText)
+ CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
+ tint = LocalCustomColors.current.itemText)
+ }
+ DropdownMenu(
+ expanded = isDropDownExpanded.value,
+ onDismissRequest = {
+ isDropDownExpanded.value = false
+ }) {
+ var index = 0
+ mediaNatMap.forEach {
+ DropdownMenuItem(text = {
+ Text(text = it.value)
+ },
+ onClick = {
+ isDropDownExpanded.value = false
+ onMediaNatSelected(it.key)
+ newMediaNat = it.key
+ })
+ if (index < 3)
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = LocalCustomColors.current.itemText
+ )
+ index++
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun StunServer() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ var stunServer by remember { mutableStateOf(oldStunServer) }
+ OutlinedTextField(
+ value = stunServer,
+ placeholder = { Text(stringResource(R.string.stun_server)) },
+ onValueChange = {
+ stunServer = it
+ newStunServer = stunServer
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.stun_server)
+ alertMessage.value = ctx.getString(R.string.stun_server_help)
+ showAlert.value = true
+ },
+ textStyle = TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.stun_server)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun StunUser() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ var stunUser by remember { mutableStateOf(oldStunUser) }
+ OutlinedTextField(
+ value = stunUser,
+ placeholder = { Text(stringResource(R.string.stun_username)) },
+ onValueChange = {
+ stunUser = it
+ newStunUser = stunUser
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.stun_username)
+ alertMessage.value = ctx.getString(R.string.stun_username_help)
+ showAlert.value = true
+ },
+ textStyle = TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.stun_username)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun StunPass() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ val showPassword = remember { mutableStateOf(false) }
+ var stunPass by remember { mutableStateOf(oldStunPass) }
+ OutlinedTextField(
+ value = stunPass,
+ placeholder = { Text(stringResource(R.string.stun_password)) },
+ onValueChange = {
+ stunPass = it
+ newStunPass = stunPass
+ },
+ singleLine = true,
+ visualTransformation = if (showPassword.value)
+ VisualTransformation.None
+ else
+ PasswordVisualTransformation(),
+ trailingIcon = {
+ val (icon, iconColor) = if (showPassword.value) {
+ Pair(
+ ImageVector.vectorResource(R.drawable.visibility),
+ colorResource(id = R.color.colorAccent)
+ )
+ } else {
+ Pair(
+ ImageVector.vectorResource(R.drawable.visibility_off),
+ colorResource(id = R.color.colorWhite)
+ )
+ }
+ IconButton(onClick = { showPassword.value = !showPassword.value }) {
+ Icon(
+ icon,
+ contentDescription = "Visibility",
+ tint = iconColor
+ )
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.stun_password)
+ alertMessage.value = ctx.getString(R.string.stun_password_help)
+ showAlert.value = true
+ },
+ textStyle = TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText
+ ),
+ label = { LabelText(stringResource(R.string.stun_password)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun RtcpMux() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.rtcp_mux),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.rtcp_mux)
+ alertMessage.value = ctx.getString(R.string.rtcp_mux_help)
+ showAlert.value = true
+ },
+ fontSize = 18.sp,
+ color = LocalCustomColors.current.itemText)
+ var rtcpMux by remember { mutableStateOf(oldRtcpMux) }
+ Switch(
+ checked = rtcpMux,
+ onCheckedChange = {
+ rtcpMux = it
+ newRtcpMux = rtcpMux
+ }
+ )
+ }
+}
+
+@Composable
+private fun Rel100() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.rel_100),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.rel_100)
+ alertMessage.value = ctx.getString(R.string.rel_100_help)
+ showAlert.value = true
+ },
+ fontSize = 18.sp,
+ color = LocalCustomColors.current.itemText)
+ var rel100 by remember { mutableStateOf(old100Rel) }
+ Switch(
+ checked = rel100,
+ onCheckedChange = {
+ rel100 = it
+ new100Rel = rel100
+ }
+ )
+ }
+}
+
+@Composable
+private fun Dtmf() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ val dtmfModeMap = mapOf(Api.DTMFMODE_RTP_EVENT to ctx.getString(R.string.dtmf_inband),
+ Api.DTMFMODE_SIP_INFO to ctx.getString(R.string.dtmf_info),
+ Api.DTMFMODE_AUTO to ctx.getString(R.string.dtmf_auto))
+ Text(text = stringResource(R.string.dtmf_mode),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.dtmf_mode)
+ alertMessage.value = ctx.getString(R.string.dtmf_mode_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ val isDropDownExpanded = remember {
+ mutableStateOf(false)
+ }
+ val dtmfMode = remember { mutableIntStateOf(oldDtmfMode) }
+ Box {
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.clickable {
+ isDropDownExpanded.value = true
+ }
+ ) {
+ Text(text = dtmfModeMap[dtmfMode.intValue]!!,
+ color = LocalCustomColors.current.itemText)
+ CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
+ tint = LocalCustomColors.current.itemText)
+ }
+ DropdownMenu(
+ expanded = isDropDownExpanded.value,
+ onDismissRequest = {
+ isDropDownExpanded.value = false
+ }) {
+ var index = 0
+ dtmfModeMap.forEach {
+ DropdownMenuItem(text = {
+ Text(text = it.value)
+ },
+ onClick = {
+ isDropDownExpanded.value = false
+ dtmfMode.intValue = it.key
+ newDtmfMode = dtmfMode.intValue
+ })
+ if (index < 2)
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = LocalCustomColors.current.itemText
+ )
+ index++
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun Answer() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ val answerModeMap = mapOf(Api.ANSWERMODE_MANUAL to ctx.getString(R.string.manual),
+ Api.ANSWERMODE_AUTO to ctx.getString(R.string.auto))
+ Text(text = stringResource(R.string.answer_mode),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.answer_mode)
+ alertMessage.value = ctx.getString(R.string.answer_mode_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ val isDropDownExpanded = remember {
+ mutableStateOf(false)
+ }
+ val answerMode = remember { mutableIntStateOf(oldAnswerMode) }
+ Box {
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.clickable {
+ isDropDownExpanded.value = true
+ }
+ ) {
+ Text(text = answerModeMap[answerMode.intValue]!!,
+ color = LocalCustomColors.current.itemText)
+ CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
+ tint = LocalCustomColors.current.itemText)
+ }
+ DropdownMenu(
+ expanded = isDropDownExpanded.value,
+ onDismissRequest = {
+ isDropDownExpanded.value = false
+ }) {
+ var index = 0
+ answerModeMap.forEach {
+ DropdownMenuItem(text = { Text(text = it.value) },
+ onClick = {
+ isDropDownExpanded.value = false
+ answerMode.intValue = it.key
+ newAnswerMode = answerMode.intValue
+ })
+ if (index < 1)
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = LocalCustomColors.current.itemText
+ )
+ index++
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun Redirect() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ val redirectModeMap = mapOf(false to ctx.getString(R.string.manual),
+ true to ctx.getString(R.string.auto))
+ Text(text = stringResource(R.string.redirect_mode),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.redirect_mode)
+ alertMessage.value = ctx.getString(R.string.redirect_mode_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ val isDropDownExpanded = remember {
+ mutableStateOf(false)
+ }
+ val autoRedirect = remember { mutableStateOf(oldAutoRedirect) }
+ Box {
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.clickable {
+ isDropDownExpanded.value = true
+ }
+ ) {
+ Text(text = redirectModeMap[autoRedirect.value]!!,
+ color = LocalCustomColors.current.itemText)
+ CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
+ tint = LocalCustomColors.current.itemText)
+ }
+ DropdownMenu(
+ expanded = isDropDownExpanded.value,
+ onDismissRequest = {
+ isDropDownExpanded.value = false
+ }) {
+ var index = 0
+ redirectModeMap.forEach {
+ DropdownMenuItem(text = {
+ Text(text = it.value)
+ },
+ onClick = {
+ isDropDownExpanded.value = false
+ autoRedirect.value = it.key
+ newAutoRedirect = autoRedirect.value
+ })
+ if (index < 1)
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = LocalCustomColors.current.itemText
+ )
+ index++
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun Voicemail() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ var vmUri by remember { mutableStateOf(oldVmUri) }
+ OutlinedTextField(
+ value = vmUri,
+ placeholder = { Text(stringResource(R.string.voicemail_uri)) },
+ onValueChange = {
+ vmUri = it
+ newVmUri = vmUri
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.voicemail_uri)
+ alertMessage.value = ctx.getString(R.string.voicemain_uri_help)
+ showAlert.value = true
+ },
+ textStyle = TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.voicemail_uri)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun CountryCode() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ var countryCode by remember { mutableStateOf(oldCountryCode) }
+ OutlinedTextField(
+ value = countryCode,
+ placeholder = { Text(stringResource(R.string.country_code)) },
+ onValueChange = {
+ countryCode = it
+ newCountryCode = countryCode
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.country_code)
+ alertMessage.value = ctx.getString(R.string.country_code_help)
+ showAlert.value = true
+ },
+ textStyle = TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.country_code)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun TelProvider() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ var telProvider by remember { mutableStateOf(oldTelProvider) }
+ OutlinedTextField(
+ value = telProvider,
+ placeholder = { Text(stringResource(R.string.telephony_provider)) },
+ onValueChange = {
+ telProvider = it
+ newTelProvider = telProvider
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.telephony_provider)
+ alertMessage.value = ctx.getString(R.string.telephony_provider_help)
+ showAlert.value = true
+ },
+ textStyle = TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.telephony_provider)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun NumericKeypad(acc: Account) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.numeric_keypad),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.numeric_keypad)
+ alertMessage.value = ctx.getString(R.string.numeric_keypad_help)
+ showAlert.value = true
+ },
+ fontSize = 18.sp,
+ color = LocalCustomColors.current.itemText)
+ var numericKeypad by remember { mutableStateOf(acc.numericKeypad) }
+ Switch(
+ checked = numericKeypad,
+ onCheckedChange = {
+ numericKeypad = it
+ newNumericKeypad = numericKeypad
+ }
+ )
+ }
+}
+
+@Composable
+private fun DefaultAccount() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.default_account),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.default_account)
+ alertMessage.value = ctx.getString(R.string.default_account_help)
+ showAlert.value = true
+ },
+ fontSize = 18.sp,
+ color = LocalCustomColors.current.itemText)
+ var defaultAccount by remember { mutableStateOf(oldDefaultAccount) }
+ Switch(
+ checked = defaultAccount,
+ onCheckedChange = {
+ defaultAccount = it
+ newDefaultAccount = defaultAccount
+ }
+ )
+ }
+}
+
+private fun checkOnClick(ctx: Context, ua: UserAgent): Boolean {
+
+ val acc = ua.account
+
+ val nn = newNickname.trim()
+ if (nn != oldNickname) {
+ if (Account.checkDisplayName(nn)) {
+ if (nn == "" || Account.uniqueNickName(nn)) {
+ acc.nickName.value = nn
+ Log.d(TAG, "New nickname is ${acc.nickName.value}")
+ }
+ else {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.non_unique_account_nickname), nn)
+ showAlert.value = true
+ return false
+ }
+ }
+ else {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_account_nickname), nn)
+ showAlert.value = true
+ return false
+ }
+ }
+
+ val dn = newDisplayname.trim()
+ if (dn != acc.displayName) {
+ if (Account.checkDisplayName(dn)) {
+ if (Api.account_set_display_name(acc.accp, dn) == 0) {
+ acc.displayName = Api.account_display_name(acc.accp)
+ Log.d(TAG, "New display name is ${acc.displayName}")
+ } else {
+ Log.e(TAG, "Setting of display name failed")
+ }
+ }
+ else {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_display_name), dn)
+ showAlert.value = true
+ return false
+ }
+ }
+
+ val au = newAuthUser.trim()
+ if (au != oldAuthUser) {
+ if (Account.checkAuthUser(au)) {
+ if (Api.account_set_auth_user(acc.accp, au) == 0) {
+ acc.authUser = Api.account_auth_user(acc.accp)
+ Log.d(TAG, "New auth user is ${acc.authUser}")
+ if (acc.regint > 0)
+ reRegister = true
+ }
+ else {
+ Log.e(TAG, "Setting of auth user failed")
+ }
+ }
+ else {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_authentication_username), au)
+ showAlert.value = true
+ return false
+ }
+ }
+
+ val ap = newAuthPass.trim()
+ if (ap != "") {
+ if (ap != oldAuthPass) {
+ if (Account.checkAuthPass(ap)) {
+ if (Api.account_set_auth_pass(acc.accp, ap) == 0) {
+ acc.authPass = Api.account_auth_pass(acc.accp)
+ if (acc.regint > 0)
+ reRegister = true
+ }
+ else
+ Log.e(TAG, "Setting of auth pass failed")
+ BaresipService.aorPasswords.remove(acc.aor)
+ }
+ else {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_authentication_password), ap)
+ showAlert.value = true
+ return false
+ }
+ }
+ else
+ BaresipService.aorPasswords.remove(acc.aor)
+ }
+ else { // ap == ""
+ if (acc.authPass != NO_AUTH_PASS && acc.authPass != BaresipService.aorPasswords[acc.aor])
+ if (Api.account_set_auth_pass(acc.accp, "") == 0) {
+ acc.authPass = NO_AUTH_PASS
+ BaresipService.aorPasswords[acc.aor] = NO_AUTH_PASS
+ }
+ }
+
+ val ob = ArrayList()
+ var ob1 = newOutbound1.trim().replace(" ", "")
+ if (ob1 != "") {
+ if (!ob1.startsWith("sip:"))
+ ob1 = "sip:$ob1"
+ if (checkOutboundUri(ob1)) {
+ ob.add(ob1)
+ }
+ else {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_proxy_server_uri), ob1)
+ showAlert.value = true
+ return false
+ }
+ }
+ var ob2 = newOutbound2.trim().replace(" ", "")
+ if (ob2 != "") {
+ if (!ob2.startsWith("sip:"))
+ ob2 = "sip:$ob2"
+ if (checkOutboundUri(ob2))
+ ob.add(ob2)
+ else {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_proxy_server_uri), ob2)
+ showAlert.value = true
+ return false
+ }
+ }
+ if (ob != acc.outbound) {
+ for (i in 0..1) {
+ val uri = if (ob.size > i)
+ ob[i]
+ else
+ ""
+ if (Api.account_set_outbound(acc.accp, uri, i) != 0)
+ Log.e(TAG, "Setting of outbound proxy $i uri '$uri' failed")
+ }
+ Log.d(TAG, "New outbound proxies are $ob")
+ acc.outbound = ob
+ if (ob.isEmpty())
+ Api.account_set_sipnat(acc.accp, "")
+ else
+ Api.account_set_sipnat(acc.accp, "outbound")
+ if (acc.regint > 0)
+ reRegister = true
+ }
+
+ val regInt = newRegInt.trim().toInt()
+ if (regInt < 60 || regInt > 3600) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_reg_int), "$regInt")
+ showAlert.value = true
+ return false
+ }
+ val reReg = (newRegister != acc.regint > 0) ||
+ (newRegister && regInt != acc.configuredRegInt)
+ if (reReg) {
+ if (Api.account_set_regint(acc.accp,
+ if (newRegister) regInt else 0) != 0) {
+ Log.e(TAG, "Setting of regint failed")
+ } else {
+ acc.regint = Api.account_regint(acc.accp)
+ acc.configuredRegInt = regInt
+ Log.d(TAG, "New regint is ${acc.regint}")
+ reRegister = true
+ }
+ } else {
+ if (regInt != acc.configuredRegInt) {
+ acc.configuredRegInt = regInt
+ }
+ }
+
+ if (newMediaEnc != acc.mediaEnc) {
+ if (Api.account_set_mediaenc(acc.accp, newMediaEnc) == 0) {
+ acc.mediaEnc = Api.account_mediaenc(acc.accp)
+ Log.d(TAG, "New mediaenc is ${acc.mediaEnc}")
+ } else {
+ Log.e(TAG, "Setting of mediaenc $newMediaEnc failed")
+ }
+ }
+
+ if (newMediaNat != acc.mediaNat) {
+ if (Api.account_set_medianat(acc.accp, newMediaNat) == 0) {
+ acc.mediaNat = Api.account_medianat(acc.accp)
+ Log.d(TAG, "New medianat is ${acc.mediaNat}")
+ } else {
+ Log.e(TAG, "Setting of medianat $newMediaNat failed")
+ }
+ }
+
+ newStunServer = newStunServer.trim()
+
+ if (newMediaNat != "") {
+ if (((newMediaNat == "stun") || (newMediaNat == "ice")) && (newStunServer == ""))
+ newStunServer = ctx.getString(R.string.stun_server_default)
+ if (!Utils.checkStunUri(newStunServer) ||
+ (newMediaNat == "turn" &&
+ newStunServer.substringBefore(":") !in setOf("turn", "turns"))) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_stun_server), newStunServer)
+ showAlert.value = true
+ return false
+ }
+ }
+
+ if (acc.stunServer != newStunServer) {
+ if (Api.account_set_stun_uri(acc.accp, newStunServer) == 0) {
+ acc.stunServer = Api.account_stun_uri(acc.accp)
+ Log.d(TAG, "New STUN/TURN server URI is '${acc.stunServer}'")
+ } else {
+ Log.e(TAG, "Setting of STUN/TURN URI server failed")
+ }
+ }
+
+ newStunUser = newStunUser.trim()
+ if (acc.stunUser != newStunUser) {
+ if (Account.checkAuthUser(newStunUser)) {
+ if (Api.account_set_stun_user(acc.accp, newStunUser) == 0) {
+ acc.stunUser = Api.account_stun_user(acc.accp)
+ Log.d(TAG, "New STUN/TURN user is ${acc.stunUser}")
+ }
+ else
+ Log.e(TAG, "Setting of STUN/TURN user failed")
+ }
+ else {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_stun_username), newStunUser)
+ showAlert.value = true
+ return false
+ }
+ }
+
+ val newStunPass = newStunPass.trim()
+ if (acc.stunPass != newStunPass) {
+ if (newStunPass.isEmpty() || Account.checkAuthPass(newStunPass)) {
+ if (Api.account_set_stun_pass(acc.accp, newStunPass) == 0)
+ acc.stunPass = Api.account_stun_pass(acc.accp)
+ else
+ Log.e(TAG, "Setting of stun pass failed")
+ }
+ else {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_stun_password), newStunPass)
+ showAlert.value = true
+ return false
+ }
+ }
+
+ if (newRtcpMux != acc.rtcpMux)
+ if (Api.account_set_rtcp_mux(acc.accp, newRtcpMux) == 0) {
+ acc.rtcpMux = Api.account_rtcp_mux(acc.accp)
+ Log.d(TAG, "New rtcpMux is ${acc.rtcpMux}")
+ } else {
+ Log.e(TAG, "Setting of account_rtc_mux $newRtcpMux failed")
+ }
+
+ if (new100Rel != (acc.rel100Mode == Api.REL100_ENABLED)) {
+ val mode = if (new100Rel) Api.REL100_ENABLED else Api.REL100_DISABLED
+ if (Api.account_set_rel100_mode(acc.accp, mode) == 0) {
+ acc.rel100Mode = Api.account_rel100_mode(acc.accp)
+ Api.ua_update_account(ua.uap)
+ Log.d(TAG, "New rel100Mode is ${acc.rel100Mode}")
+ } else {
+ Log.e(TAG, "Setting of account_rel100Mode failed")
+ }
+ }
+
+ if (newDtmfMode != acc.dtmfMode) {
+ if (Api.account_set_dtmfmode(acc.accp, newDtmfMode) == 0) {
+ acc.dtmfMode = Api.account_dtmfmode(acc.accp)
+ Log.d(TAG, "New dtmfmode is ${acc.dtmfMode}")
+ } else {
+ Log.e(TAG, "Setting of dtmfmode $newDtmfMode failed")
+ }
+ }
+
+ if (newAnswerMode != acc.answerMode) {
+ if (Api.account_set_answermode(acc.accp, newAnswerMode) == 0) {
+ acc.answerMode = Api.account_answermode(acc.accp)
+ Log.d(TAG, "New answermode is ${acc.answerMode}")
+ } else {
+ Log.e(TAG, "Setting of answermode $newAnswerMode failed")
+ }
+ }
+
+ if (newAutoRedirect != acc.autoRedirect) {
+ Api.account_set_sip_autoredirect(acc.accp, newAutoRedirect)
+ acc.autoRedirect = newAutoRedirect
+ Log.d(TAG, "New autoRedirect is ${acc.autoRedirect}")
+ }
+
+ newVmUri = newVmUri.trim()
+ if (newVmUri != acc.vmUri) {
+ if (newVmUri != "") {
+ if (!newVmUri.startsWith("sip:")) newVmUri = "sip:$newVmUri"
+ if (!newVmUri.contains("@")) newVmUri = "$newVmUri@${acc.host()}"
+ if (!Utils.checkUri(newVmUri)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), newVmUri)
+ showAlert.value = true
+ return false
+ }
+ Api.account_set_mwi(acc.accp, true)
+ }
+ else
+ Api.account_set_mwi(acc.accp, false)
+ acc.vmUri = newVmUri
+ Log.d(TAG, "New voicemail URI is ${acc.vmUri}")
+ }
+
+ newCountryCode = newCountryCode.trim()
+ if (newCountryCode != acc.countryCode) {
+ if (newCountryCode != "" && !Utils.checkCountryCode(newCountryCode)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_country_code), newCountryCode)
+ showAlert.value = true
+ return false
+ }
+ acc.countryCode = newCountryCode
+ Log.d(TAG, "New country code is ${acc.countryCode}")
+ }
+
+ val hostPart = newTelProvider.trim()
+ if (hostPart != acc.telProvider) {
+ if (hostPart != "" && !Utils.checkHostPortParams(hostPart)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_uri_hostpart), hostPart)
+ showAlert.value = true
+ return false
+ }
+ acc.telProvider = hostPart
+ Log.d(TAG, "New tel provider is ${acc.telProvider}")
+ }
+
+ if (newNumericKeypad != acc.numericKeypad) {
+ acc.numericKeypad = newNumericKeypad
+ Log.d(TAG, "New numericKeyboard is ${acc.numericKeypad}")
+ }
+
+ if (newDefaultAccount) ua.makeDefault()
+
+ Account.saveAccounts()
+
+ if (acc.authUser != "" && BaresipService.aorPasswords[acc.aor] == NO_AUTH_PASS) {
+ showPasswordDialog.value = true
+ return false
+ }
+ else
+ return true
+}
+
+private fun checkOutboundUri(uri: String): Boolean {
+ if (!uri.startsWith("sip:")) return false
+ return Utils.checkHostPortParams(uri.substring(4))
+}
+
+private fun initAccountFromConfig(acc: Account, onConfigLoaded: () -> Unit) {
+ val scope = CoroutineScope(Job() + Dispatchers.Main)
+ scope.launch(Dispatchers.IO) {
+ val url = "https://${Utils.uriHostPart(acc.aor)}/baresip/account_config.xml"
+ val config = try {
+ URL(url).readText()
+ } catch (e: java.lang.Exception) {
+ Log.d(TAG, "Failed to get account configuration from network: ${e.message}")
+ null
+ }
+ if (config != null) {
+ Log.d(TAG, "Got account config '$config'")
+ val parserFactory: XmlPullParserFactory = XmlPullParserFactory.newInstance()
+ val parser: XmlPullParser = parserFactory.newPullParser()
+ parser.setInput(StringReader(config))
+ var tag: String?
+ var text = ""
+ var event = parser.eventType
+ val audioCodecs = ArrayList(Api.audio_codecs().split(","))
+ val videoCodecs = ArrayList(Api.video_codecs().split(","))
+
+ while (event != XmlPullParser.END_DOCUMENT) {
+ tag = parser.name
+ when (event) {
+ XmlPullParser.TEXT ->
+ text = parser.text
+
+ XmlPullParser.START_TAG -> {
+ if (tag == "audio-codecs")
+ acc.audioCodec.clear()
+ if (tag == "video-codecs")
+ acc.videoCodec.clear()
+ }
+
+ XmlPullParser.END_TAG ->
+ when (tag) {
+ "outbound-proxy-1" ->
+ if (text.isNotEmpty())
+ acc.outbound.add(text)
+
+ "outbound-proxy-2" ->
+ if (text.isNotEmpty())
+ acc.outbound.add(text)
+
+ "registration-interval" ->
+ acc.configuredRegInt = text.toInt()
+
+ "register" ->
+ acc.regint = if (text == "yes") acc.configuredRegInt else 0
+
+ "audio-codec" ->
+ if (text in audioCodecs)
+ acc.audioCodec.add(text)
+
+ "video-codec" ->
+ if (text in videoCodecs)
+ acc.videoCodec.add(text)
+
+ "media-encoding" -> {
+ val enc = text.lowercase(Locale.ROOT)
+ if (enc in mediaEncMap.keys && enc.isNotEmpty())
+ acc.mediaEnc = enc
+ }
+
+ "media-nat" -> {
+ val nat = text.lowercase(Locale.ROOT)
+ if (nat in mediaNatMap.keys && nat.isNotEmpty())
+ acc.mediaNat = nat
+ }
+
+ "stun-turn-server" ->
+ if (text.isNotEmpty())
+ acc.stunServer = text
+
+ "rtcp-mux" ->
+ acc.rtcpMux = text == "yes"
+
+ "100rel-mode" ->
+ acc.rel100Mode = if (text == "yes")
+ Api.REL100_ENABLED
+ else
+ Api.REL100_DISABLED
+
+ "dtmf-mode" ->
+ if (text in arrayOf("rtp-event", "sip-info", "auto"))
+ acc.dtmfMode = when (text) {
+ "rtp-event" -> Api.DTMFMODE_RTP_EVENT
+ "sip-info" -> Api.DTMFMODE_SIP_INFO
+ else -> Api.DTMFMODE_AUTO
+ }
+
+ "answer-mode" ->
+ if (text in arrayOf("manual", "auto"))
+ acc.answerMode = if (text == "manual")
+ Api.ANSWERMODE_MANUAL
+ else
+ Api.ANSWERMODE_AUTO
+
+ "redirect-mode" ->
+ acc.autoRedirect = text == "yes"
+
+ "voicemail-uri" ->
+ if (text.isNotEmpty())
+ acc.vmUri = text
+
+ "country-code" ->
+ acc.countryCode = text
+
+ "tel-provider" ->
+ acc.telProvider = text
+ }
+ }
+ event = parser.next()
+ }
+ }
+ onConfigLoaded()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountsActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountsActivity.kt
deleted file mode 100644
index c1659734..00000000
--- a/app/src/main/kotlin/com/tutpro/baresip/AccountsActivity.kt
+++ /dev/null
@@ -1,403 +0,0 @@
-package com.tutpro.baresip
-
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import android.os.Bundle
-import android.os.SystemClock
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.navigationBarsPadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.Delete
-import androidx.compose.material.icons.outlined.Clear
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SmallFloatingActionButton
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import com.tutpro.baresip.CustomElements.AlertDialog
-import com.tutpro.baresip.CustomElements.LabelText
-import com.tutpro.baresip.CustomElements.verticalScrollbar
-
-class AccountsActivity : ComponentActivity() {
-
- internal lateinit var aor: String
- private lateinit var mediaEncMap: Map
- private lateinit var mediaNatMap: Map
-
- private var showAccounts = mutableStateOf(true)
- private var lastClick: Long = 0
-
- private val alertTitle = mutableStateOf("")
- private val alertMessage = mutableStateOf("")
- private val showAlert = mutableStateOf(false)
-
- private var backInvokedCallback: OnBackInvokedCallback? = null
- private lateinit var onBackPressedCallback: OnBackPressedCallback
-
- @RequiresApi(33)
- private fun registerBackInvokedCallback() {
- backInvokedCallback = OnBackInvokedCallback { goBack() }
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- backInvokedCallback!!
- )
- }
-
- public override fun onCreate(savedInstanceState: Bundle?) {
-
- super.onCreate(savedInstanceState)
-
- enableEdgeToEdge()
-
- if (Build.VERSION.SDK_INT >= 33)
- registerBackInvokedCallback()
- else {
- onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- goBack()
- }
- }
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
- val title = getString(R.string.accounts)
-
- aor = intent.getStringExtra("aor")!!
- Utils.addActivity("accounts,$aor")
-
- mediaEncMap = mapOf("zrtp" to "ZRTP", "dtls_srtp" to "DTLS-SRTPF",
- "srtp-mand" to "SRTP-MAND", "srtp" to "SRTP", "" to "--")
-
- mediaNatMap = mapOf("stun" to "STUN", "turn" to "TURN", "ice" to "ICE", "" to "--")
-
- setContent {
- AppTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = LocalCustomColors.current.background
- ) {
- AccountsScreen(this, title) { goBack() }
- }
- }
- }
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- fun AccountsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
- Scaffold(
- modifier = Modifier.fillMaxHeight()
- .imePadding()
- .safeDrawingPadding(),
- containerColor = LocalCustomColors.current.background,
- topBar = {
- TopAppBar(
- title = {
- Text(text = title,
- color = LocalCustomColors.current.light,
- fontWeight = FontWeight.Bold
- )
- },
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = LocalCustomColors.current.primary
- ),
- navigationIcon = {
- IconButton(onClick = navigateBack) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = LocalCustomColors.current.light
- )
- }
- },
- )
- },
- bottomBar = { NewAccount(ctx) },
- content = { contentPadding ->
- AccountsContent(ctx, contentPadding)
- },
- )
- }
-
- @Composable
- fun AccountsContent(ctx: Context, contentPadding: PaddingValues) {
-
- val showDialog = remember { mutableStateOf(false) }
- val message = remember { mutableStateOf("") }
- val positiveAction = remember { mutableStateOf({}) }
-
- AlertDialog(
- showDialog = showDialog,
- title = stringResource(R.string.confirmation),
- message = message.value,
- positiveButtonText = stringResource(R.string.delete),
- onPositiveClicked = positiveAction.value,
- negativeButtonText = stringResource(R.string.cancel),
- )
-
- if (showAlert.value) {
- AlertDialog(
- showDialog = showAlert,
- title = alertTitle.value,
- message = alertMessage.value,
- positiveButtonText = stringResource(R.string.ok),
- )
- }
-
- if (showAccounts.value && BaresipService.uas.value.isNotEmpty()) {
- val scrollState = rememberScrollState()
- Column(
- modifier = Modifier
- .imePadding()
- .fillMaxWidth()
- .padding(contentPadding)
- .padding(start = 16.dp, end = 4.dp, top = 8.dp, bottom = 8.dp)
- .verticalScrollbar(scrollState)
- .verticalScroll(state = scrollState),
- verticalArrangement = Arrangement.spacedBy(4.dp),
- ) {
- for (ua in BaresipService.uas.value) {
- val account = ua.account
- val aor = account.aor
- val text = if (account.nickName.value != "")
- account.nickName.value
- else
- account.aor.substringAfter(":")
- Row(
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- text = text,
- fontSize = 20.sp,
- color = LocalCustomColors.current.itemText,
- modifier = Modifier.weight(1f).padding(start = 10.dp)
- .clickable {
- val i = Intent(ctx, AccountActivity::class.java)
- val b = Bundle()
- b.putString("aor", aor)
- i.putExtras(b)
- startActivity(i)
- }
- )
- SmallFloatingActionButton(
- onClick = {
- if (SystemClock.elapsedRealtime() - lastClick > 1000) {
- lastClick = SystemClock.elapsedRealtime()
- message.value = String.format(
- ctx.getString(R.string.delete_account),
- text
- )
- positiveAction.value = {
- CallHistoryNew.clear(aor)
- Message.clearMessagesOfAor(aor)
- ua.remove()
- Api.ua_destroy(ua.uap)
- saveAccounts()
- showAccounts.value = false
- showAccounts.value = true
- }
- showDialog.value = true
- }
- },
- containerColor = LocalCustomColors.current.background,
- contentColor = LocalCustomColors.current.secondary
- ) {
- Icon(
- Icons.Filled.Delete,
- contentDescription = stringResource(R.string.delete)
- )
- }
- }
- }
- }
- }
- }
-
- @Composable
- fun NewAccount(ctx: Context) {
- var newAor by remember { mutableStateOf("") }
- val focusManager = LocalFocusManager.current
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .navigationBarsPadding()
- .padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- OutlinedTextField(
- value = newAor,
- placeholder = { Text(text = stringResource(R.string.new_account)) },
- onValueChange = { newAor = it },
- modifier = Modifier
- .weight(1f)
- .padding(end = 8.dp)
- .verticalScroll(rememberScrollState())
- .clickable {
- alertTitle.value = getString(R.string.new_account)
- alertMessage.value = getString(R.string.accounts_help)
- showAlert.value = true
- },
- singleLine = false,
- trailingIcon = {
- if (newAor.isNotEmpty()) {
- Icon(
- Icons.Outlined.Clear,
- contentDescription = "Clear",
- modifier = Modifier.clickable { newAor = "" }
- )
- }
- },
- label = { LabelText(stringResource(R.string.new_account)) },
- textStyle = TextStyle(fontSize = 18.sp),
- keyboardOptions = KeyboardOptions(
- keyboardType = KeyboardType.Text,
- )
- )
- Image(
- painter = painterResource(id = R.drawable.plus),
- contentDescription = "Add",
- contentScale = ContentScale.Crop,
- modifier = Modifier.size(48.dp)
- .clickable(
- onClick = {
- val account = createNew(newAor.trim())
- if (account != null) {
- val i = Intent(ctx, AccountActivity::class.java)
- val b = Bundle()
- b.putString("aor", account.aor)
- b.putString("kind", "new")
- i.putExtras(b)
- startActivity(i)
- newAor = ""
- focusManager.clearFocus()
- showAccounts.value = false
- }
- }
- ),
- )
- }
- }
-
- private fun createNew(newAor: String): Account? {
-
- val aor = if (newAor.startsWith("sip:"))
- newAor
- else
- "sip:$newAor"
-
- if (!Utils.checkAor(aor)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_aor), aor.split(":")[1])
- showAlert.value = true
- return null
- }
-
- if (Account.ofAor(aor) != null) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.account_exists), aor.split(":")[1])
- showAlert.value = true
- return null
- }
-
- val ua = UserAgent.uaAlloc(
- "<$aor>;stunserver=\"stun:stun.l.google.com:19302\";regq=0.5;pubint=0;regint=0;mwi=no"
- )
- if (ua == null) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = getString(R.string.account_allocation_failure)
- showAlert.value = true
- return null
- }
-
- // Api.account_debug(ua.account.accp)
- val acc = ua.account
- Log.d(TAG, "Allocated UA ${ua.uap} with SIP URI ${acc.luri}")
- saveAccounts()
-
- return acc
- }
-
- private fun goBack() {
- BaresipService.activities.remove("accounts,$aor")
- setResult(RESULT_CANCELED, Intent())
- finish()
- }
-
- override fun onPause() {
- MainActivity.activityAor = aor
- super.onPause()
- }
-
- override fun onResume() {
- super.onResume()
- showAccounts.value = true
- }
-
- override fun onDestroy() {
- if (Build.VERSION.SDK_INT >= 33) {
- if (backInvokedCallback != null)
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
- }
- else
- onBackPressedCallback.remove()
- super.onDestroy()
- }
-
- companion object {
-
- fun saveAccounts() {
- var accounts = ""
- for (a in Account.accounts()) accounts = accounts + a.print() + "\n"
- Utils.putFileContents(BaresipService.filesPath + "/accounts",
- accounts.toByteArray(Charsets.UTF_8))
- // Log.d(TAG, "Saved accounts '${accounts}' to '${BaresipService.filesPath}/accounts'")
- }
-
- }
-
-}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountsScreen.kt
new file mode 100644
index 00000000..e3329f1e
--- /dev/null
+++ b/app/src/main/kotlin/com/tutpro/baresip/AccountsScreen.kt
@@ -0,0 +1,306 @@
+package com.tutpro.baresip
+
+import android.content.Context
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.outlined.Clear
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SmallFloatingActionButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import com.tutpro.baresip.CustomElements.AlertDialog
+import com.tutpro.baresip.CustomElements.LabelText
+import com.tutpro.baresip.CustomElements.verticalScrollbar
+
+fun NavGraphBuilder.accountsScreenRoute(navController: NavController) {
+ composable("accounts") {
+ AccountsScreen(navController)
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AccountsScreen(navController: NavController) {
+ Scaffold(
+ modifier = Modifier.fillMaxSize().imePadding(),
+ containerColor = LocalCustomColors.current.background,
+ topBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(
+ top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
+ )
+ ) {
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(R.string.accounts),
+ color = LocalCustomColors.current.light,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = { navController.popBackStack() }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = null,
+ tint = LocalCustomColors.current.light
+ )
+ }
+ },
+ colors = TopAppBarDefaults.mediumTopAppBarColors(
+ containerColor = LocalCustomColors.current.primary
+ ),
+ windowInsets = WindowInsets(0, 0, 0, 0)
+ )
+ }
+ },
+ bottomBar = { NewAccount(navController) },
+ content = { contentPadding ->
+ AccountsContent(contentPadding, navController)
+ },
+ )
+}
+
+
+@Composable
+fun AccountsContent(contentPadding: PaddingValues, navController: NavController) {
+
+ val ctx = LocalContext.current
+
+ val showDialog = remember { mutableStateOf(false) }
+ val message = remember { mutableStateOf("") }
+ val positiveAction = remember { mutableStateOf({}) }
+
+ AlertDialog(
+ showDialog = showDialog,
+ title = stringResource(R.string.confirmation),
+ message = message.value,
+ positiveButtonText = stringResource(R.string.delete),
+ onPositiveClicked = positiveAction.value,
+ negativeButtonText = stringResource(R.string.cancel),
+ )
+
+ val showAccounts = remember { mutableStateOf(true) }
+
+ if (showAccounts.value && BaresipService.uas.value.isNotEmpty()) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(contentPadding)
+ .padding(start = 16.dp, end = 4.dp, top = 8.dp, bottom = 8.dp)
+ .verticalScrollbar(scrollState)
+ .verticalScroll(state = scrollState),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ for (ua in BaresipService.uas.value) {
+ val account = ua.account
+ val aor = account.aor
+ val text = if (account.nickName.value != "")
+ account.nickName.value
+ else
+ account.aor.substringAfter(":")
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = text,
+ fontSize = 20.sp,
+ color = LocalCustomColors.current.itemText,
+ modifier = Modifier.weight(1f).padding(start = 10.dp)
+ .clickable {
+ navController.navigate("account/$aor/old")
+ }
+ )
+ SmallFloatingActionButton(
+ onClick = {
+ message.value = String.format(
+ ctx.getString(R.string.delete_account),
+ text
+ )
+ positiveAction.value = {
+ CallHistoryNew.clear(aor)
+ Message.clearMessagesOfAor(aor)
+ ua.remove()
+ Api.ua_destroy(ua.uap)
+ Account.saveAccounts()
+ showAccounts.value = false
+ showAccounts.value = true
+ }
+ showDialog.value = true
+ },
+ containerColor = LocalCustomColors.current.background,
+ contentColor = LocalCustomColors.current.secondary
+ ) {
+ Icon(
+ Icons.Filled.Delete,
+ contentDescription = stringResource(R.string.delete)
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun NewAccount(navController: NavController) {
+
+ val alertTitle = remember { mutableStateOf("") }
+ val alertMessage = remember { mutableStateOf("") }
+ val showAlert = remember { mutableStateOf(false) }
+
+ if (showAlert.value)
+ AlertDialog(
+ showDialog = showAlert,
+ title = alertTitle.value,
+ message = alertMessage.value,
+ positiveButtonText = stringResource(R.string.ok),
+ )
+
+ fun createNew(ctx: Context, newAor: String): Account? {
+
+ val aor = if (newAor.startsWith("sip:"))
+ newAor
+ else
+ "sip:$newAor"
+
+ if (!Utils.checkAor(aor)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value =
+ String.format(ctx.getString(R.string.invalid_aor), aor.split(":")[1])
+ showAlert.value = true
+ return null
+ }
+
+ if (Account.ofAor(aor) != null) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value =
+ String.format(ctx.getString(R.string.account_exists), aor.split(":")[1])
+ showAlert.value = true
+ return null
+ }
+
+ val ua = UserAgent.uaAlloc(
+ "<$aor>;stunserver=\"stun:stun.l.google.com:19302\";regq=0.5;pubint=0;regint=0;mwi=no"
+ )
+ if (ua == null) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = ctx.getString(R.string.account_allocation_failure)
+ showAlert.value = true
+ return null
+ }
+
+ // Api.account_debug(ua.account.accp)
+ val acc = ua.account
+ Log.d(TAG, "Allocated UA ${ua.uap} with SIP URI ${acc.luri}")
+ Account.saveAccounts()
+ return acc
+ } // createNew
+
+ var newAor by remember { mutableStateOf("") }
+ val focusManager = LocalFocusManager.current
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .navigationBarsPadding()
+ .padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val ctx = LocalContext.current
+ OutlinedTextField(
+ value = newAor,
+ placeholder = { Text(text = stringResource(R.string.new_account)) },
+ onValueChange = { newAor = it },
+ modifier = Modifier
+ .weight(1f)
+ .padding(end = 8.dp)
+ .verticalScroll(rememberScrollState())
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.new_account)
+ alertMessage.value = ctx.getString(R.string.accounts_help)
+ showAlert.value = true
+ },
+ singleLine = false,
+ trailingIcon = {
+ if (newAor.isNotEmpty()) {
+ Icon(
+ Icons.Outlined.Clear,
+ contentDescription = "Clear",
+ modifier = Modifier.clickable { newAor = "" }
+ )
+ }
+ },
+ label = { LabelText(stringResource(R.string.new_account)) },
+ textStyle = TextStyle(fontSize = 18.sp),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ )
+ )
+ Image(
+ painter = painterResource(id = R.drawable.plus),
+ contentDescription = "Add",
+ contentScale = ContentScale.Crop,
+ modifier = Modifier.size(48.dp)
+ .clickable(
+ onClick = {
+ val account = createNew(ctx, newAor.trim())
+ if (account != null) {
+ navController.navigate("account/${account.aor}/new")
+ newAor = ""
+ focusManager.clearFocus()
+ }
+ }
+ )
+ )
+ }
+}
+
diff --git a/app/src/main/kotlin/com/tutpro/baresip/AndroidContactActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/AndroidContactActivity.kt
deleted file mode 100644
index dc792e09..00000000
--- a/app/src/main/kotlin/com/tutpro/baresip/AndroidContactActivity.kt
+++ /dev/null
@@ -1,305 +0,0 @@
-package com.tutpro.baresip
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import android.os.Build
-import android.os.Bundle
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.graphics.SolidColor
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import coil.compose.AsyncImage
-
-class AndroidContactActivity : ComponentActivity() {
-
- private lateinit var aor: String
- private lateinit var name: String
-
- private var color = 0
-
- private var backInvokedCallback: OnBackInvokedCallback? = null
- private lateinit var onBackPressedCallback: OnBackPressedCallback
-
- @RequiresApi(33)
- private fun registerBackInvokedCallback() {
- backInvokedCallback = OnBackInvokedCallback { goBack() }
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- backInvokedCallback!!
- )
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
-
- super.onCreate(savedInstanceState)
-
- enableEdgeToEdge()
-
- if (Build.VERSION.SDK_INT >= 33)
- registerBackInvokedCallback()
- else {
- onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- goBack()
- }
- }
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
- aor = intent.getStringExtra("aor")!!
- name = intent.getStringExtra("name")!!
-
- val title: String = name
- val contact = Contact.androidContact(name)
-
- if (contact == null) {
- Log.e(TAG, "No Android contact found with name $name")
- goBack()
- }
-
- Utils.addActivity("android contact, $name")
-
- setContent {
- AppTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = LocalCustomColors.current.background
- ) {
- ContactScreen(this, title, contact!!) { goBack() }
- }
- }
- }
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- fun ContactScreen(ctx: Context, title: String, contact: Contact.AndroidContact,
- navigateBack: () -> Unit) {
- Scaffold(
- modifier = Modifier.fillMaxHeight().imePadding().safeDrawingPadding(),
- containerColor = LocalCustomColors.current.background,
- topBar = {
- TopAppBar(
- title = {
- Text(
- text = title, color = LocalCustomColors.current.grayLight,
- fontWeight = FontWeight.Bold
- )
- },
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = LocalCustomColors.current.primary
- ),
- navigationIcon = {
- IconButton(onClick = navigateBack) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = LocalCustomColors.current.light
- )
- }
- },
- )
- },
- content = { contentPadding ->
- ContactContent(ctx, contentPadding, contact)
- }
- )
- }
-
- @Composable
- fun ContactContent(ctx: Context, contentPadding: PaddingValues, contact: Contact.AndroidContact) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .background(LocalCustomColors.current.background)
- .padding(contentPadding)
- .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp),
- verticalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- Avatar(contact)
- ContactName()
- Uris(ctx, contact)
- }
- }
-
- @Composable
- fun TextAvatar(text: String, size: Int, color: Int) {
- Box(
- modifier = Modifier.size(size.dp),
- contentAlignment = Alignment.Center
- ) {
- Canvas(modifier = Modifier.fillMaxSize()) {
- drawCircle(SolidColor(Color(color)))
- }
- Text(text, fontSize = 72.sp, color = Color.White)
- }
- }
-
- @Composable
- fun ImageAvatar(uri: Uri, size: Int) {
- AsyncImage(
- model = uri,
- contentDescription = stringResource(R.string.avatar_image),
- contentScale = ContentScale.Crop,
- modifier = Modifier.size(size.dp).clip(CircleShape)
- )
- }
-
- @Composable
- fun Avatar(contact: Contact.AndroidContact) {
- Row(
- Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) {
- color = contact.color
- val thumbnailUri = contact.thumbnailUri
- if (thumbnailUri != null)
- ImageAvatar(thumbnailUri, 96)
- else
- TextAvatar(if (name == "") "" else name[0].toString(), 96, color)
- }
- }
-
- @Composable
- fun ContactName() {
- Row(
- Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 8.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(name, fontSize = 24.sp, color = LocalCustomColors.current.itemText)
- }
- }
-
- @Composable
- fun Uris(ctx: Context, contact: Contact.AndroidContact) {
- val lazyListState = rememberLazyListState()
- LazyColumn(
- modifier = Modifier
- .imePadding()
- .fillMaxWidth()
- .padding(start = 16.dp, end = 4.dp)
- .background(LocalCustomColors.current.background),
- state = lazyListState,
- verticalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- items(contact.uris) { uri ->
- Row(
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- text = uri.substringAfter(":"),
- modifier = Modifier.weight(1f),
- fontSize = 18.sp,
- color = LocalCustomColors.current.itemText,
- )
- Image(
- painter = painterResource(R.drawable.message),
- colorFilter = ColorFilter.tint(LocalCustomColors.current.itemText),
- contentDescription = "Send Message",
- modifier = Modifier.padding(end = 24.dp).clickable {
- val i = Intent(ctx, MainActivity::class.java)
- i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
- Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
- i.putExtra("action", "message")
- val ua = UserAgent.ofAor(aor)
- if (ua == null) {
- Log.w(TAG, "message clickable did not find AoR $aor")
- } else {
- BaresipService.activities.clear()
- i.putExtra("uap", ua.uap)
- i.putExtra("peer", uri)
- (ctx as Activity).startActivity(i)
- }
- }
- )
- Image(
- painter = painterResource(R.drawable.call_small),
- colorFilter = ColorFilter.tint(LocalCustomColors.current.itemText),
- contentDescription = "Call",
- modifier = Modifier.clickable {
- val i = Intent(ctx, MainActivity::class.java)
- i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
- Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
- i.putExtra("action", "call")
- val ua = UserAgent.ofAor(aor)
- if (ua == null) {
- Log.w(TAG, "call clickable did not find AoR $aor")
- } else {
- BaresipService.activities.clear()
- i.putExtra("uap", ua.uap)
- i.putExtra("peer", uri)
- (ctx as Activity).startActivity(i)
- }
- }
- )
- }
- }
- }
- }
-
- override fun onDestroy() {
- if (Build.VERSION.SDK_INT >= 33) {
- if (backInvokedCallback != null)
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
- }
- else
- onBackPressedCallback.remove()
- super.onDestroy()
- }
-
- private fun goBack() {
- BaresipService.activities.remove("android contact,$name")
- setResult(RESULT_CANCELED, Intent(this, MainActivity::class.java))
- finish()
- }
-}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/AndroidContactScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AndroidContactScreen.kt
new file mode 100644
index 00000000..aa3d4176
--- /dev/null
+++ b/app/src/main/kotlin/com/tutpro/baresip/AndroidContactScreen.kt
@@ -0,0 +1,272 @@
+package com.tutpro.baresip
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import coil.compose.AsyncImage
+
+fun NavGraphBuilder.androidContactScreenRoute(navController: NavController, viewModel: ViewModel) {
+ composable(
+ route = "android_contact/{name}",
+ arguments = listOf(navArgument("name") { type = NavType.StringType })
+ ) { backStackEntry ->
+ val ctx = LocalContext.current
+ val name = backStackEntry.arguments?.getString("name")!!
+ ContactScreen(
+ ctx = ctx,
+ viewModel = viewModel,
+ navController = navController,
+ name = name
+ )
+ }
+}
+
+@Composable
+private fun ContactScreen(ctx: Context, viewModel: ViewModel, navController: NavController, name: String) {
+ val contact = Contact.androidContact(name)
+ if (contact == null) {
+ Log.e(TAG, "No Android contact found with name $name")
+ navController.popBackStack()
+ }
+ Scaffold(
+ modifier = Modifier.fillMaxSize().imePadding(),
+ containerColor = LocalCustomColors.current.background,
+ topBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
+ ) {
+ TopAppBar(name, navController)
+ }
+ },
+ content = { contentPadding ->
+ ContactContent(ctx, viewModel, navController, contentPadding, contact!!)
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TopAppBar(title: String, navController: NavController) {
+ TopAppBar(
+ title = {
+ Text(
+ text = title,
+ color = LocalCustomColors.current.light,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ colors = TopAppBarDefaults.mediumTopAppBarColors(
+ containerColor = LocalCustomColors.current.primary
+ ),
+ windowInsets = WindowInsets(0, 0, 0, 0),
+ navigationIcon = {
+ IconButton(onClick = { navController.popBackStack() }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back",
+ tint = LocalCustomColors.current.light
+ )
+ }
+ }
+ )
+}
+
+@Composable
+private fun ContactContent(
+ ctx: Context,
+ viewModel: ViewModel,
+ navController: NavController,
+ contentPadding: PaddingValues,
+ contact: Contact.AndroidContact
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(contentPadding)
+ .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ Avatar(contact)
+ ContactName(contact.name)
+ Uris(ctx, viewModel, navController, contact)
+ }
+}
+
+@Composable
+private fun TextAvatar(text: String, color: Int) {
+ Box(
+ modifier = Modifier.size(avatarSize.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Canvas(modifier = Modifier.fillMaxSize()) {
+ drawCircle(SolidColor(Color(color)))
+ }
+ Text(text, fontSize = 72.sp, color = Color.White)
+ }
+}
+
+@Composable
+private fun ImageAvatar(uri: Uri) {
+ AsyncImage(
+ model = uri,
+ contentDescription = stringResource(R.string.avatar_image),
+ contentScale = ContentScale.Crop,
+ modifier = Modifier.size(avatarSize.dp).clip(CircleShape)
+ )
+}
+
+@Composable
+private fun Avatar(contact: Contact.AndroidContact) {
+ Row(
+ Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ val color = contact.color
+ val name = contact.name
+ val thumbnailUri = contact.thumbnailUri
+ if (thumbnailUri != null)
+ ImageAvatar(thumbnailUri)
+ else
+ TextAvatar(if (name == "") "" else name[0].toString(), color)
+ }
+}
+
+@Composable
+private fun ContactName(name: String) {
+ Row(
+ Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ Text(name, fontSize = 24.sp, color = LocalCustomColors.current.itemText)
+ }
+}
+
+@Composable
+private fun Uris(
+ ctx: Context,
+ viewModel: ViewModel,
+ navController: NavController,
+ contact: Contact.AndroidContact
+) {
+ val lazyListState = rememberLazyListState()
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, end = 4.dp)
+ .background(LocalCustomColors.current.background),
+ state = lazyListState,
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ items(contact.uris) { uri ->
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = uri.substringAfter(":"),
+ modifier = Modifier.weight(1f),
+ fontSize = 18.sp,
+ color = LocalCustomColors.current.itemText,
+ )
+ Image(
+ painter = painterResource(R.drawable.message),
+ colorFilter = ColorFilter.tint(LocalCustomColors.current.itemText),
+ contentDescription = "Send Message",
+ modifier = Modifier.padding(end = 24.dp).clickable {
+ val aor = viewModel.selectedAor.value
+ val ua = UserAgent.ofAor(aor)
+ if (ua == null)
+ Log.w(TAG, "message clickable did not find AoR $aor")
+ else {
+ val intent = Intent(ctx, MainActivity::class.java)
+ intent.putExtra("uap", ua.uap)
+ intent.putExtra("peer", uri)
+ handleIntent(ctx, viewModel, intent, "message")
+ navController.navigate("main") {
+ popUpTo("main") { inclusive = false }
+ launchSingleTop = true
+ }
+ }
+ }
+ )
+ Image(
+ painter = painterResource(R.drawable.call_small),
+ colorFilter = ColorFilter.tint(LocalCustomColors.current.itemText),
+ contentDescription = "Call",
+ modifier = Modifier.clickable {
+ val aor = viewModel.selectedAor.value
+ val ua = UserAgent.ofAor(aor)
+ if (ua == null)
+ Log.w(TAG, "message clickable did not find AoR $aor")
+ else {
+ val intent = Intent(ctx, MainActivity::class.java)
+ intent.putExtra("uap", ua.uap)
+ intent.putExtra("peer", uri)
+ handleIntent(ctx, viewModel, intent, "call")
+ navController.navigate("main") {
+ popUpTo("main") { inclusive = false }
+ launchSingleTop = true
+ }
+ }
+ }
+ )
+ }
+ }
+ }
+}
+
+
diff --git a/app/src/main/kotlin/com/tutpro/baresip/AppTheme.kt b/app/src/main/kotlin/com/tutpro/baresip/AppTheme.kt
index 10515dde..97e1a1f9 100644
--- a/app/src/main/kotlin/com/tutpro/baresip/AppTheme.kt
+++ b/app/src/main/kotlin/com/tutpro/baresip/AppTheme.kt
@@ -2,6 +2,7 @@ package com.tutpro.baresip
import android.app.Activity
import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
@@ -10,59 +11,61 @@ import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
+import androidx.core.graphics.ColorUtils
import androidx.core.view.WindowCompat
@Composable
fun AppTheme(
content: @Composable () -> Unit
) {
- val darkTheme = remember { BaresipService.darkTheme }
+ val useDarkTheme by remember { BaresipService.darkTheme }
- // "normal" palette, nothing change here
val colorScheme = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
- if (darkTheme.value) dynamicDarkColorScheme(context = LocalContext.current)
- else dynamicLightColorScheme(context = LocalContext.current)
+ if (useDarkTheme)
+ dynamicDarkColorScheme(context = LocalContext.current)
+ else
+ if (isSystemInDarkTheme())
+ dynamicDarkColorScheme(context = LocalContext.current)
+ else
+ dynamicLightColorScheme(context = LocalContext.current)
}
- darkTheme.value -> darkColorScheme()
- else -> lightColorScheme()
+ useDarkTheme -> darkColorScheme()
+ else ->
+ if (isSystemInDarkTheme())
+ darkColorScheme()
+ else
+ lightColorScheme()
}
- // logic for which custom palette to use
val customColorsPalette =
- if (darkTheme.value) DarkCustomColors
+ if (useDarkTheme) DarkCustomColors
else LightCustomColors
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
- val decorView = window.decorView
+ val insetsController = WindowCompat.getInsetsController(window, view)
- // Ensure insets are applied correctly
- WindowCompat.setDecorFitsSystemWindows(window, false)
+ // A common threshold for luminance is 0.5. Colors with luminance > 0.5 are considered light.
+ val isBackgroundEffectivelyLight = ColorUtils.calculateLuminance(customColorsPalette.background.toArgb()) > 0.5
- // Handle the status bar appearance
- val insetsController = WindowCompat.getInsetsController(window, decorView)
- window.statusBarColor = customColorsPalette.background.toArgb()
- window.navigationBarColor = customColorsPalette.background.toArgb()
- insetsController.apply {
- isAppearanceLightStatusBars = !darkTheme.value
- isAppearanceLightNavigationBars = !darkTheme.value
- }
+ insetsController.isAppearanceLightStatusBars = isBackgroundEffectivelyLight
+ insetsController.isAppearanceLightNavigationBars = isBackgroundEffectivelyLight
}
}
- // here is the important point, where you will expose custom objects
CompositionLocalProvider(
- LocalCustomColors provides customColorsPalette // our custom palette
+ LocalCustomColors provides customColorsPalette
) {
MaterialTheme(
- colorScheme = colorScheme, // the MaterialTheme still uses the "normal" palette
+ colorScheme = colorScheme, // MaterialTheme still uses the "normal" colorScheme
content = content
)
}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/AudioActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/AudioActivity.kt
deleted file mode 100644
index ffb27ef2..00000000
--- a/app/src/main/kotlin/com/tutpro/baresip/AudioActivity.kt
+++ /dev/null
@@ -1,700 +0,0 @@
-package com.tutpro.baresip
-
-import android.os.Build
-import android.os.Bundle
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Switch
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import com.tutpro.baresip.CustomElements.AlertDialog
-import com.tutpro.baresip.CustomElements.LabelText
-import com.tutpro.baresip.CustomElements.verticalScrollbar
-
-class AudioActivity : ComponentActivity() {
-
- private var save = false
- private var restart = false
- private var oldCallVolume = BaresipService.callVolume
- private var newCallVolume = oldCallVolume
- private var oldMicGain = ""
- private var newMicGain = ""
- private var newSpeakerPhone = BaresipService.speakerPhone
- private val modules = Config.variables("module")
- private var newAudioModules = mutableMapOf()
- private var oldOpusBitrate = Config.variable("opus_bitrate")
- private var newOpusBitrate = oldOpusBitrate
- private var oldOpusPacketLoss = Config.variable("opus_packet_loss")
- private var newOpusPacketLoss = oldOpusPacketLoss
- private var newAudioDelay = BaresipService.audioDelay.toString()
- private var newToneCountry = BaresipService.toneCountry
-
- private val alertTitle = mutableStateOf("")
- private val alertMessage = mutableStateOf("")
- private val showAlert = mutableStateOf(false)
-
- private var backInvokedCallback: OnBackInvokedCallback? = null
- private lateinit var onBackPressedCallback: OnBackPressedCallback
-
- @RequiresApi(33)
- private fun registerBackInvokedCallback() {
- backInvokedCallback = OnBackInvokedCallback { goBack() }
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- backInvokedCallback!!
- )
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
-
- super.onCreate(savedInstanceState)
-
- enableEdgeToEdge()
-
- if (Build.VERSION.SDK_INT >= 33)
- registerBackInvokedCallback()
- else {
- onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- goBack()
- }
- }
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
- Utils.addActivity("audio")
-
- if (!BaresipService.agcAvailable)
- oldMicGain = Config.variable("augain")
-
- setContent {
- AppTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = LocalCustomColors.current.background
- ) {
- AudioScreen { goBack() }
- }
- }
- }
- }
-
- @Composable
- fun AudioScreen(navigateBack: () -> Unit) {
- Scaffold(
- modifier = Modifier
- .fillMaxHeight()
- .imePadding()
- .safeDrawingPadding(),
- containerColor = LocalCustomColors.current.background,
- topBar = { TopAppBar(navigateBack) },
- content = { contentPadding ->
- AudioContent(contentPadding)
- }
- )
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- fun TopAppBar(navigateBack: () -> Unit) {
- androidx.compose.material3.TopAppBar(
- title = {
- Text(
- text = stringResource(R.string.audio_settings),
- color = LocalCustomColors.current.light,
- fontWeight = FontWeight.Bold
- )
- },
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = LocalCustomColors.current.primary
- ),
- navigationIcon = {
- IconButton(onClick = navigateBack) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = LocalCustomColors.current.light
- )
- }
- },
- actions = {
- IconButton(onClick = {
- checkOnClick()
- }) {
- Icon(
- imageVector = Icons.Filled.Check,
- tint = LocalCustomColors.current.light,
- contentDescription = "Check"
- )
- }
- }
- )
- }
-
- @Composable
- fun AudioContent(contentPadding: PaddingValues) {
-
- if (showAlert.value) {
- AlertDialog(
- showDialog = showAlert,
- title = alertTitle.value,
- message = alertMessage.value,
- positiveButtonText = stringResource(R.string.ok),
- )
- }
- val scrollState = rememberScrollState()
-
- Column(
- modifier = Modifier
- .imePadding()
- .fillMaxWidth()
- .padding(contentPadding)
- .padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 4.dp)
- .verticalScrollbar(scrollState)
- .verticalScroll(state = scrollState),
- verticalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- CallVolume()
- MicGain()
- SpeakerPhone()
- AudioModules()
- OpusBitRate()
- OpusPacketLoss()
- AudioDelay()
- ToneCountry()
- }
- }
-
- @Composable
- private fun CallVolume() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.default_call_volume),
- modifier = Modifier
- .weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.default_call_volume)
- alertMessage.value = getString(R.string.default_call_volume_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- val isDropDownExpanded = remember {
- mutableStateOf(false)
- }
- val volNames = listOf("--", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10")
- val volValues = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
- val itemPosition = remember {
- mutableIntStateOf(volValues.indexOf(oldCallVolume))
- }
- Box {
- Row(
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.clickable {
- isDropDownExpanded.value = true
- }
- ) {
- Text(text = volNames[itemPosition.intValue],
- color = LocalCustomColors.current.itemText)
- CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
- tint = LocalCustomColors.current.itemText)
- }
- DropdownMenu(
- expanded = isDropDownExpanded.value,
- onDismissRequest = {
- isDropDownExpanded.value = false
- }) {
- volNames.forEachIndexed { index, vol ->
- DropdownMenuItem(text = {
- Text(text = vol)
- },
- onClick = {
- isDropDownExpanded.value = false
- itemPosition.intValue = index
- newCallVolume = volValues[index]
- })
- if (index < 10)
- HorizontalDivider(
- thickness = 1.dp,
- color = LocalCustomColors.current.itemText
- )
- }
- }
- }
- }
- }
-
- @Composable
- private fun MicGain() {
- if (!BaresipService.agcAvailable)
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var micGain by remember { mutableStateOf(oldMicGain) }
- newMicGain = micGain
- OutlinedTextField(
- value = micGain,
- placeholder = { Text(stringResource(R.string.microphone_gain)) },
- onValueChange = {
- micGain = it
- newMicGain = micGain
- },
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.microphone_gain)
- alertMessage.value = getString(R.string.microphone_gain_help)
- showAlert.value = true
- },
- textStyle = androidx.compose.ui.text.TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText
- ),
- label = { LabelText(stringResource(R.string.microphone_gain)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- private fun SpeakerPhone() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.speaker_phone),
- modifier = Modifier
- .weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.speaker_phone)
- alertMessage.value = getString(R.string.speaker_phone_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- var speakerPhone by remember { mutableStateOf(BaresipService.speakerPhone) }
- Switch(
- checked = speakerPhone,
- onCheckedChange = {
- speakerPhone = it
- newSpeakerPhone = speakerPhone
- }
- )
- }
- }
-
- @Composable
- private fun AudioModules() {
- Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.Start,
- ) {
- Text(text = stringResource(R.string.audio_modules_title),
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp,
- modifier = Modifier.clickable {
- alertTitle.value = getString(R.string.audio_modules_title)
- alertMessage.value = getString(R.string.audio_modules_help)
- showAlert.value = true
- })
- for (module in audioModules) {
- Row(horizontalArrangement = Arrangement.Start,
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(start = 18.dp, end = 10.dp)
- ) {
- Text(text = String.format(getString(R.string.bullet_item), module),
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- Spacer(modifier = Modifier.weight(1f))
- var checked by remember { mutableStateOf(modules.contains("${module}.so")) }
- Switch(
- checked = checked,
- onCheckedChange = {
- checked = it
- newAudioModules[module] = checked
- }
- )
- }
- }
- }
- }
-
- @Composable
- private fun OpusBitRate() {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var opusBitrate by remember { mutableStateOf(oldOpusBitrate) }
- newOpusBitrate = opusBitrate
- OutlinedTextField(
- value = opusBitrate,
- placeholder = { Text(stringResource(R.string.opus_bit_rate)) },
- onValueChange = {
- opusBitrate = it
- newOpusBitrate = opusBitrate
- },
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.opus_bit_rate)
- alertMessage.value = getString(R.string.opus_bit_rate_help)
- showAlert.value = true
- },
- textStyle = androidx.compose.ui.text.TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.opus_bit_rate)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- private fun OpusPacketLoss() {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp, top = 8.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var opusPacketLoss by remember { mutableStateOf(oldOpusPacketLoss) }
- newOpusPacketLoss = opusPacketLoss
- OutlinedTextField(
- value = opusPacketLoss,
- placeholder = { Text(stringResource(R.string.opus_packet_loss)) },
- onValueChange = {
- opusPacketLoss = it
- newOpusPacketLoss = opusPacketLoss
- },
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.opus_packet_loss)
- alertMessage.value = getString(R.string.opus_packet_loss_help)
- showAlert.value = true
- },
- textStyle = androidx.compose.ui.text.TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.opus_packet_loss)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- private fun AudioDelay() {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp, top = 8.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var audioDelay by remember { mutableStateOf(BaresipService.audioDelay.toString()) }
- newAudioDelay = audioDelay
- OutlinedTextField(
- value = audioDelay,
- placeholder = { Text(getString(R.string.audio_delay)) },
- onValueChange = {
- audioDelay = it
- newAudioDelay = audioDelay
- },
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.audio_delay)
- alertMessage.value = getString(R.string.audio_delay_help)
- showAlert.value = true
- },
- textStyle = androidx.compose.ui.text.TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.audio_delay)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- private fun ToneCountry() {
- Row(
- Modifier.fillMaxWidth().padding(end=10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.tone_country),
- modifier = Modifier
- .weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.tone_country)
- alertMessage.value = getString(R.string.tone_country_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- val isDropDownExpanded = remember {
- mutableStateOf(false)
- }
- val countryNames = arrayListOf("BG", "BR", "DE", "CZ", "ES", "FI", "FR", "GB", "JP", "NO", "NZ", "SE", "RU", "US")
- val countryValues = arrayListOf("bg", "br", "de", "cz", "es", "fi", "fr", "uk", "jp", "no", "nz", "se", "ru", "us")
- val itemPosition = remember {
- mutableIntStateOf(countryValues.indexOf(BaresipService.toneCountry))
- }
- Box {
- Row(
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.clickable {
- isDropDownExpanded.value = true
- }
- ) {
- Text(text = countryNames[itemPosition.intValue],
- color = LocalCustomColors.current.itemText)
- CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
- tint = LocalCustomColors.current.itemText)
- }
- DropdownMenu(
- expanded = isDropDownExpanded.value,
- onDismissRequest = {
- isDropDownExpanded.value = false
- }) {
- countryNames.forEachIndexed { index, name ->
- DropdownMenuItem(text = {
- Text(text = name)
- },
- onClick = {
- isDropDownExpanded.value = false
- itemPosition.intValue = index
- newToneCountry = countryValues[index]
- })
- if (index < 10)
- HorizontalDivider(
- thickness = 1.dp,
- color = LocalCustomColors.current.itemText
- )
- }
- }
- }
- }
- }
-
- private fun checkOnClick() {
-
- if (BaresipService.activities.indexOf("audio") == -1)
- return
-
- if (BaresipService.callVolume != newCallVolume) {
- BaresipService.callVolume = newCallVolume
- Config.replaceVariable("call_volume", newCallVolume.toString())
- save = true
- }
-
- if (!BaresipService.agcAvailable) {
- var gain = newMicGain.trim()
- if (!gain.contains("."))
- gain = "$gain.0"
- if (gain != oldMicGain) {
- if (!checkMicGain(gain)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = "${getString(R.string.invalid_microphone_gain)}: $gain."
- showAlert.value = true
- return
- }
- if (gain == "1.0") {
- Api.module_unload("augain")
- Config.removeVariableValue("module", "augain.so")
- Config.replaceVariable("augain", "1.0")
- } else {
- if (oldMicGain == "1.0") {
- if (Api.module_load("augain") != 0) {
- alertTitle.value = getString(R.string.error)
- alertMessage.value = getString(R.string.failed_to_load_module) + ": augain.so"
- showAlert.value = true
- return
- }
- Config.addVariable("module", "augain.so")
- }
- Config.replaceVariable("augain", gain)
- Api.cmd_exec("augain $gain")
- }
- save = true
- }
- }
-
- if (newSpeakerPhone != BaresipService.speakerPhone) {
- BaresipService.speakerPhone = newSpeakerPhone
- Config.replaceVariable("speaker_phone",
- if (BaresipService.speakerPhone) "yes" else "no")
- save = true
- }
-
- for (module in audioModules) {
- if (newAudioModules[module] != null) {
- if (newAudioModules[module]!!) {
- if (!modules.contains("${module}.so")) {
- if (Api.module_load("${module}.so") != 0) {
- alertTitle.value = getString(R.string.error)
- alertMessage.value = "${getString(R.string.failed_to_load_module)}: ${module}.so"
- showAlert.value = true
- return
- }
- Config.addVariable("module", "${module}.so")
- save = true
- }
- } else if (modules.contains("${module}.so")) {
- Api.module_unload("${module}.so")
- Config.removeVariableValue("module", "${module}.so")
- for (ua in BaresipService.uas.value)
- ua.account.removeAudioCodecs(module)
- AccountsActivity.saveAccounts()
- save = true
- }
- }
- }
-
- if (newOpusBitrate != oldOpusBitrate) {
- if (!checkOpusBitRate(newOpusBitrate)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = "${getString(R.string.invalid_opus_bitrate)}: $newOpusBitrate."
- showAlert.value = true
- return
- }
- Config.replaceVariable("opus_bitrate", newOpusBitrate)
- restart = true
- save = true
- }
-
- if (newOpusPacketLoss != oldOpusPacketLoss) {
- if (!checkOpusPacketLoss(newOpusPacketLoss)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = "${getString(R.string.invalid_opus_packet_loss)}: $newOpusPacketLoss"
- showAlert.value = true
- return
- }
- Config.replaceVariable("opus_packet_loss", newOpusPacketLoss)
- restart = true
- save = true
- }
-
- val audioDelay = newAudioDelay.trim()
- if (audioDelay != BaresipService.audioDelay.toString()) {
- if (!checkAudioDelay(audioDelay)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_audio_delay), audioDelay)
- showAlert.value = true
- return
- }
- Config.replaceVariable("audio_delay", audioDelay)
- BaresipService.audioDelay = audioDelay.toLong()
- save = true
- }
-
- if (BaresipService.toneCountry != newToneCountry) {
- BaresipService.toneCountry = newToneCountry
- Config.replaceVariable("tone_country", newToneCountry)
- save = true
- }
-
- if (save)
- Config.save()
-
- setResult(if (restart) RESULT_OK else RESULT_CANCELED)
-
- BaresipService.activities.remove("audio")
- finish()
- }
-
- override fun onDestroy() {
- if (Build.VERSION.SDK_INT >= 33) {
- if (backInvokedCallback != null)
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
- }
- else
- onBackPressedCallback.remove()
- super.onDestroy()
- }
-
- private fun goBack() {
- BaresipService.activities.remove("audio")
- finish()
- }
-
- private fun checkMicGain(micGain: String): Boolean {
- val number =
- try {
- micGain.toDouble()
- } catch (_: NumberFormatException) {
- return false
- }
- return number >= 1.0
- }
-
- private fun checkOpusBitRate(opusBitRate: String): Boolean {
- val number = opusBitRate.toIntOrNull() ?: return false
- return (number >= 6000) && (number <= 510000)
- }
-
- private fun checkOpusPacketLoss(opusPacketLoss: String): Boolean {
- val number = opusPacketLoss.toIntOrNull() ?: return false
- return (number >= 0) && (number <= 100)
- }
-
- private fun checkAudioDelay(audioDelay: String): Boolean {
- val number = audioDelay.toIntOrNull() ?: return false
- return (number >= 100) && (number <= 3000)
- }
-
- companion object {
- val audioModules = listOf("opus", "amr", "g722", "g7221", "g726", "g729", "codec2", "g711")
- }
-
-}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt
new file mode 100644
index 00000000..8bd2610c
--- /dev/null
+++ b/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt
@@ -0,0 +1,664 @@
+package com.tutpro.baresip
+
+import android.content.Context
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.LocalActivity
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import com.tutpro.baresip.CustomElements.AlertDialog
+import com.tutpro.baresip.CustomElements.LabelText
+import com.tutpro.baresip.CustomElements.verticalScrollbar
+
+fun NavGraphBuilder.audioScreenRoute(
+ navController: NavController,
+) {
+ composable("audio") {
+ val activity = LocalActivity.current
+ val viewModel: ViewModel = viewModel(activity as ComponentActivity)
+ val ctx = LocalContext.current
+ AudioScreen(
+ onBack = { navController.popBackStack() },
+ checkOnClick = {
+ viewModel.setAudioSettingsResult(checkOnClick(ctx))
+ navController.popBackStack()
+ },
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun AudioScreen(
+ onBack: () -> Unit,
+ checkOnClick: () -> Unit,
+) {
+ Scaffold(
+ modifier = Modifier.fillMaxSize().imePadding(),
+ containerColor = LocalCustomColors.current.background,
+ topBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(
+ top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
+ )
+ ) {
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(R.string.audio_settings),
+ color = LocalCustomColors.current.light,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = null,
+ tint = LocalCustomColors.current.light
+ )
+ }
+ },
+ windowInsets = WindowInsets(0, 0, 0, 0),
+ actions = {
+ IconButton(onClick = checkOnClick) {
+ Icon(
+ imageVector = Icons.Filled.Check,
+ tint = LocalCustomColors.current.light,
+ contentDescription = "Check"
+ )
+ }
+ },
+ colors = TopAppBarDefaults.mediumTopAppBarColors(
+ containerColor = LocalCustomColors.current.primary
+ )
+ )
+ }
+ }
+ ) { contentPadding ->
+ AudioContent(contentPadding)
+ }
+}
+
+private var oldCallVolume = BaresipService.callVolume
+private var newCallVolume = oldCallVolume
+private var oldMicGain = ""
+private var newMicGain = ""
+private var oldSpeakerPhone = BaresipService.speakerPhone
+private var newSpeakerPhone = oldSpeakerPhone
+private var oldAudioModules = ArrayList()
+private var newAudioModules = mutableMapOf()
+private var oldOpusBitrate = ""
+private var newOpusBitrate = oldOpusBitrate
+private var oldOpusPacketLoss = ""
+private var newOpusPacketLoss = oldOpusPacketLoss
+private var newAudioDelay = BaresipService.audioDelay.toString()
+private var newToneCountry = BaresipService.toneCountry
+
+private var save = false
+
+private val alertTitle = mutableStateOf("")
+private val alertMessage = mutableStateOf("")
+private val showAlert = mutableStateOf(false)
+
+@Composable
+private fun AudioContent(contentPadding: PaddingValues) {
+
+ oldAudioModules = Config.variables("module")
+ oldOpusBitrate = Config.variable("opus_bitrate")
+ oldOpusPacketLoss = Config.variable("opus_packet_loss")
+ if (!BaresipService.agcAvailable)
+ oldMicGain = Config.variable("augain")
+
+ if (showAlert.value) {
+ AlertDialog(
+ showDialog = showAlert,
+ title = alertTitle.value,
+ message = alertMessage.value,
+ positiveButtonText = stringResource(R.string.ok),
+ )
+ }
+
+ val scrollState = rememberScrollState()
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(contentPadding)
+ .padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 4.dp)
+ .verticalScrollbar(scrollState)
+ .verticalScroll(state = scrollState),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ CallVolume()
+ MicGain()
+ SpeakerPhone()
+ AudioModules()
+ OpusBitRate()
+ OpusPacketLoss()
+ AudioDelay()
+ ToneCountry()
+ }
+}
+
+@Composable
+private fun CallVolume() {
+ Row(
+ Modifier.fillMaxWidth().padding(end=10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.default_call_volume),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.default_call_volume)
+ alertMessage.value = ctx.getString(R.string.default_call_volume_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ val isDropDownExpanded = remember {
+ mutableStateOf(false)
+ }
+ val volNames = listOf("--", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10")
+ val volValues = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+ val itemPosition = remember {
+ mutableIntStateOf(volValues.indexOf(oldCallVolume))
+ }
+ Box {
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.clickable {
+ isDropDownExpanded.value = true
+ }
+ ) {
+ Text(text = volNames[itemPosition.intValue],
+ color = LocalCustomColors.current.itemText)
+ CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
+ tint = LocalCustomColors.current.itemText)
+ }
+ DropdownMenu(
+ expanded = isDropDownExpanded.value,
+ onDismissRequest = {
+ isDropDownExpanded.value = false
+ }) {
+ volNames.forEachIndexed { index, vol ->
+ DropdownMenuItem(text = {
+ Text(text = vol)
+ },
+ onClick = {
+ isDropDownExpanded.value = false
+ itemPosition.intValue = index
+ newCallVolume = volValues[index]
+ })
+ if (index < 10)
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = LocalCustomColors.current.itemText
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun MicGain() {
+ if (!BaresipService.agcAvailable)
+ Row(
+ Modifier.fillMaxWidth().padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ var micGain by remember { mutableStateOf(oldMicGain) }
+ newMicGain = micGain
+ OutlinedTextField(
+ value = micGain,
+ placeholder = { Text(stringResource(R.string.microphone_gain)) },
+ onValueChange = {
+ micGain = it
+ newMicGain = micGain
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.microphone_gain)
+ alertMessage.value = ctx.getString(R.string.microphone_gain_help)
+ showAlert.value = true
+ },
+ textStyle = androidx.compose.ui.text.TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText
+ ),
+ label = { LabelText(stringResource(R.string.microphone_gain)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun SpeakerPhone() {
+ Row(
+ Modifier.fillMaxWidth().padding(end=10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.speaker_phone),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.speaker_phone)
+ alertMessage.value = ctx.getString(R.string.speaker_phone_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ var speakerPhone by remember { mutableStateOf(BaresipService.speakerPhone) }
+ Switch(
+ checked = speakerPhone,
+ onCheckedChange = {
+ speakerPhone = it
+ newSpeakerPhone = speakerPhone
+ }
+ )
+ }
+}
+
+@Composable
+private fun AudioModules() {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.Start,
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.audio_modules_title),
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp,
+ modifier = Modifier.clickable {
+ alertTitle.value = ctx.getString(R.string.audio_modules_title)
+ alertMessage.value = ctx.getString(R.string.audio_modules_help)
+ showAlert.value = true
+ })
+ for (module in Config.audioModules) {
+ Row(horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(start = 18.dp, end = 10.dp)
+ ) {
+ Text(text = String.format(ctx.getString(R.string.bullet_item), module),
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ Spacer(modifier = Modifier.weight(1f))
+ var checked by remember { mutableStateOf(oldAudioModules.contains("${module}.so")) }
+ Switch(
+ checked = checked,
+ onCheckedChange = {
+ checked = it
+ newAudioModules[module] = checked
+ }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun OpusBitRate() {
+ Row(
+ Modifier.fillMaxWidth().padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ var opusBitrate by remember { mutableStateOf(oldOpusBitrate) }
+ newOpusBitrate = opusBitrate
+ OutlinedTextField(
+ value = opusBitrate,
+ placeholder = { Text(stringResource(R.string.opus_bit_rate)) },
+ onValueChange = {
+ opusBitrate = it
+ newOpusBitrate = opusBitrate
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.opus_bit_rate)
+ alertMessage.value = ctx.getString(R.string.opus_bit_rate_help)
+ showAlert.value = true
+ },
+ textStyle = androidx.compose.ui.text.TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.opus_bit_rate)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun OpusPacketLoss() {
+ Row(
+ Modifier.fillMaxWidth().padding(end = 10.dp, top = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ var opusPacketLoss by remember { mutableStateOf(oldOpusPacketLoss) }
+ newOpusPacketLoss = opusPacketLoss
+ OutlinedTextField(
+ value = opusPacketLoss,
+ placeholder = { Text(stringResource(R.string.opus_packet_loss)) },
+ onValueChange = {
+ opusPacketLoss = it
+ newOpusPacketLoss = opusPacketLoss
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.opus_packet_loss)
+ alertMessage.value = ctx.getString(R.string.opus_packet_loss_help)
+ showAlert.value = true
+ },
+ textStyle = androidx.compose.ui.text.TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.opus_packet_loss)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun AudioDelay() {
+ Row(
+ Modifier.fillMaxWidth().padding(end = 10.dp, top = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ var audioDelay by remember { mutableStateOf(BaresipService.audioDelay.toString()) }
+ newAudioDelay = audioDelay
+ OutlinedTextField(
+ value = audioDelay,
+ placeholder = { Text(stringResource(R.string.audio_delay)) },
+ onValueChange = {
+ audioDelay = it
+ newAudioDelay = audioDelay
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.audio_delay)
+ alertMessage.value = ctx.getString(R.string.audio_delay_help)
+ showAlert.value = true
+ },
+ textStyle = androidx.compose.ui.text.TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.audio_delay)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun ToneCountry() {
+ Row(
+ Modifier.fillMaxWidth().padding(end=10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.tone_country),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.tone_country)
+ alertMessage.value = ctx.getString(R.string.tone_country_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ val isDropDownExpanded = remember {
+ mutableStateOf(false)
+ }
+ val countryNames = arrayListOf("BG", "BR", "DE", "CZ", "ES", "FI", "FR", "GB", "JP", "NO", "NZ", "SE", "RU", "US")
+ val countryValues = arrayListOf("bg", "br", "de", "cz", "es", "fi", "fr", "uk", "jp", "no", "nz", "se", "ru", "us")
+ val itemPosition = remember {
+ mutableIntStateOf(countryValues.indexOf(BaresipService.toneCountry))
+ }
+ Box {
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.clickable {
+ isDropDownExpanded.value = true
+ }
+ ) {
+ Text(text = countryNames[itemPosition.intValue],
+ color = LocalCustomColors.current.itemText)
+ CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
+ tint = LocalCustomColors.current.itemText)
+ }
+ DropdownMenu(
+ expanded = isDropDownExpanded.value,
+ onDismissRequest = {
+ isDropDownExpanded.value = false
+ }) {
+ countryNames.forEachIndexed { index, name ->
+ DropdownMenuItem(text = {
+ Text(text = name)
+ },
+ onClick = {
+ isDropDownExpanded.value = false
+ itemPosition.intValue = index
+ newToneCountry = countryValues[index]
+ })
+ if (index < 10)
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = LocalCustomColors.current.itemText
+ )
+ }
+ }
+ }
+ }
+}
+
+private fun checkOnClick(ctx: Context): Boolean {
+
+ var restart = false
+
+ if (BaresipService.callVolume != newCallVolume) {
+ BaresipService.callVolume = newCallVolume
+ Config.replaceVariable("call_volume", newCallVolume.toString())
+ save = true
+ }
+
+ if (!BaresipService.agcAvailable) {
+ var gain = newMicGain.trim()
+ if (!gain.contains("."))
+ gain = "$gain.0"
+ if (gain != oldMicGain) {
+ if (!checkMicGain(gain)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = "${ctx.getString(R.string.invalid_microphone_gain)}: $gain."
+ showAlert.value = true
+ return false
+ }
+ if (gain == "1.0") {
+ Api.module_unload("augain")
+ Config.removeVariableValue("module", "augain.so")
+ Config.replaceVariable("augain", "1.0")
+ } else {
+ if (oldMicGain == "1.0") {
+ if (Api.module_load("augain") != 0) {
+ alertTitle.value = ctx.getString(R.string.error)
+ alertMessage.value = ctx.getString(R.string.failed_to_load_module) + ": augain.so"
+ showAlert.value = true
+ return false
+ }
+ Config.addVariable("module", "augain.so")
+ }
+ Config.replaceVariable("augain", gain)
+ Api.cmd_exec("augain $gain")
+ }
+ save = true
+ }
+ }
+
+ if (newSpeakerPhone != BaresipService.speakerPhone) {
+ BaresipService.speakerPhone = newSpeakerPhone
+ Config.replaceVariable("speaker_phone",
+ if (BaresipService.speakerPhone) "yes" else "no")
+ save = true
+ }
+
+ for (module in Config.audioModules) {
+ if (newAudioModules[module] != null) {
+ if (newAudioModules[module]!!) {
+ if (!oldAudioModules.contains("${module}.so")) {
+ if (Api.module_load("${module}.so") != 0) {
+ alertTitle.value = ctx.getString(R.string.error)
+ alertMessage.value = "${ctx.getString(R.string.failed_to_load_module)}: ${module}.so"
+ showAlert.value = true
+ return false
+ }
+ Config.addVariable("module", "${module}.so")
+ save = true
+ }
+ } else if (oldAudioModules.contains("${module}.so")) {
+ Api.module_unload("${module}.so")
+ Config.removeVariableValue("module", "${module}.so")
+ for (ua in BaresipService.uas.value)
+ ua.account.removeAudioCodecs(module)
+ Account.saveAccounts()
+ save = true
+ }
+ }
+ }
+
+ if (newOpusBitrate != oldOpusBitrate) {
+ if (!checkOpusBitRate(newOpusBitrate)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = "${ctx.getString(R.string.invalid_opus_bitrate)}: $newOpusBitrate."
+ showAlert.value = true
+ return false
+ }
+ Config.replaceVariable("opus_bitrate", newOpusBitrate)
+ restart = true
+ save = true
+ }
+
+ if (newOpusPacketLoss != oldOpusPacketLoss) {
+ if (!checkOpusPacketLoss(newOpusPacketLoss)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = "${ctx.getString(R.string.invalid_opus_packet_loss)}: $newOpusPacketLoss"
+ showAlert.value = true
+ return false
+ }
+ Config.replaceVariable("opus_packet_loss", newOpusPacketLoss)
+ restart = true
+ save = true
+ }
+
+ val audioDelay = newAudioDelay.trim()
+ if (audioDelay != BaresipService.audioDelay.toString()) {
+ if (!checkAudioDelay(audioDelay)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_audio_delay), audioDelay)
+ showAlert.value = true
+ return false
+ }
+ Config.replaceVariable("audio_delay", audioDelay)
+ BaresipService.audioDelay = audioDelay.toLong()
+ save = true
+ }
+
+ if (BaresipService.toneCountry != newToneCountry) {
+ BaresipService.toneCountry = newToneCountry
+ Config.replaceVariable("tone_country", newToneCountry)
+ save = true
+ }
+
+ if (save) Config.save()
+
+ return restart
+}
+
+private fun checkMicGain(micGain: String): Boolean {
+ val number =
+ try {
+ micGain.toDouble()
+ } catch (_: NumberFormatException) {
+ return false
+ }
+ return number >= 1.0
+}
+
+private fun checkOpusBitRate(opusBitRate: String): Boolean {
+ val number = opusBitRate.toIntOrNull() ?: return false
+ return (number >= 6000) && (number <= 510000)
+}
+
+private fun checkOpusPacketLoss(opusPacketLoss: String): Boolean {
+ val number = opusPacketLoss.toIntOrNull() ?: return false
+ return (number >= 0) && (number <= 100)
+}
+
+private fun checkAudioDelay(audioDelay: String): Boolean {
+ val number = audioDelay.toIntOrNull() ?: return false
+ return (number >= 100) && (number <= 3000)
+}
+
diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipContactActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipContactActivity.kt
deleted file mode 100644
index 78071dc8..00000000
--- a/app/src/main/kotlin/com/tutpro/baresip/BaresipContactActivity.kt
+++ /dev/null
@@ -1,724 +0,0 @@
-package com.tutpro.baresip
-
-import android.content.ContentProviderOperation
-import android.content.ContentValues
-import android.content.Context
-import android.content.Intent
-import android.database.Cursor
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import android.graphics.Matrix
-import android.net.Uri
-import android.os.Build
-import android.os.Bundle
-import android.provider.ContactsContract
-import android.provider.ContactsContract.CommonDataKinds
-import android.provider.ContactsContract.Contacts.Data
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Switch
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.SolidColor
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.KeyboardCapitalization
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.core.graphics.scale
-import androidx.exifinterface.media.ExifInterface
-import coil.compose.rememberAsyncImagePainter
-import com.tutpro.baresip.CustomElements.AlertDialog
-import java.io.ByteArrayOutputStream
-import java.io.File
-
-class BaresipContactActivity : ComponentActivity() {
-
- private lateinit var name: String
- private lateinit var uri: String
- private var new = false
- private var favorite = false
- private var android = false
- private var newName = ""
- private var newUri = ""
- private var newFavorite = false
- private var newAndroid = false
- private var uriOrName = ""
- private var color = 0
- private var id: Long = 0
- private var newId: Long = 0
- private var tmpFile: File? = null
-
- private val alertTitle = mutableStateOf("")
- private val alertMessage = mutableStateOf("")
- private val showAlert = mutableStateOf(false)
-
- private val textAvatarText = mutableStateOf("")
- private val textAvatarColor = mutableIntStateOf(0)
- private val imageAvatarUri = mutableStateOf("")
-
- private var backInvokedCallback: OnBackInvokedCallback? = null
- private lateinit var onBackPressedCallback: OnBackPressedCallback
-
- @RequiresApi(33)
- private fun registerBackInvokedCallback() {
- backInvokedCallback = OnBackInvokedCallback { goBack() }
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- backInvokedCallback!!
- )
- }
-
- public override fun onCreate(savedInstanceState: Bundle?) {
-
- super.onCreate(savedInstanceState)
-
- enableEdgeToEdge()
-
- if (Build.VERSION.SDK_INT >= 33)
- registerBackInvokedCallback()
- else {
- onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- goBack()
- }
- }
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
- val title: String
-
- new = intent.getBooleanExtra("new", false)
-
- if (new) {
- name = ""
- uri = intent.getStringExtra("uri")!!
- favorite = false
- android = BaresipService.contactsMode == "android"
- title = getString(R.string.new_contact)
- uriOrName = uri
- color = Utils.randomColor()
- id = System.currentTimeMillis()
- newId = id
- }
- else {
- name = intent.getStringExtra("name")!!
- val contact = Contact.baresipContact(name)!!
- uri = contact.uri
- favorite = contact.favorite
- android = false
- title = name
- uriOrName = name
- color = contact.color
- id = contact.id
- newId = id
- if (contact.avatarImage != null) {
- val avatarFile = File(BaresipService.filesPath, "${newId}.png")
- if (avatarFile.exists())
- imageAvatarUri.value = Uri.fromFile(avatarFile).toString()
- }
- }
-
- Utils.addActivity("baresip contact,$new,$uriOrName")
-
- setContent {
- AppTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = LocalCustomColors.current.background
- ) {
- ContactScreen(this, title) { goBack() }
- }
- }
- }
- }
-
- @Composable
- fun ContactScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
- Scaffold(
- modifier = Modifier.fillMaxHeight().imePadding().safeDrawingPadding(),
- containerColor = LocalCustomColors.current.background,
- topBar = { TopAppBar(ctx, title, navigateBack) },
- content = { contentPadding ->
- ContactContent(contentPadding)
- }
- )
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- fun TopAppBar(ctx: Context, title: String, navigateBack: () -> Unit) {
- TopAppBar(
- title = {
- Text(
- text = title,
- color = LocalCustomColors.current.light,
- fontWeight = FontWeight.Bold
- )
- },
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = LocalCustomColors.current.primary
- ),
- navigationIcon = {
- IconButton(onClick = navigateBack) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = LocalCustomColors.current.light
- )
- }
- },
- actions = {
- IconButton(onClick = {
- checkOnClick(ctx)
- }) {
- Icon(
- imageVector = Icons.Filled.Check,
- tint = LocalCustomColors.current.light,
- contentDescription = "Check"
- )
- }
- }
- )
- }
-
- @OptIn(ExperimentalFoundationApi::class)
- @Composable
- fun ContactContent(contentPadding: PaddingValues) {
-
- if (showAlert.value) {
- AlertDialog(
- showDialog = showAlert,
- title = alertTitle.value,
- message = alertMessage.value,
- positiveButtonText = stringResource(R.string.ok),
- )
- }
-
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .background(LocalCustomColors.current.background)
- .padding(contentPadding)
- .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp),
- verticalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- Avatar()
- ContactName()
- ContactUri()
- Favorite()
- Android()
- }
- }
-
-
- @Composable
- fun TextAvatar(size: Int) {
- Box(
- modifier = Modifier.size(size.dp),
- contentAlignment = Alignment.Center
- ) {
- Canvas(modifier = Modifier.fillMaxSize()) {
- drawCircle(SolidColor(Color(textAvatarColor.intValue)))
- }
- Text(textAvatarText.value, fontSize = 72.sp, color = Color.White)
- }
- }
-
- @Composable
- fun ImageAvatar(size: Int) {
- Image(
- painter = rememberAsyncImagePainter(model = imageAvatarUri.value),
- contentDescription = stringResource(R.string.avatar_image),
- contentScale = ContentScale.Crop,
- modifier = Modifier.size(size.dp).clip(CircleShape)
- )
- }
-
- @OptIn(ExperimentalFoundationApi::class)
- @Composable
- fun Avatar() {
- val avatarRequest =
- rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
- if (it != null)
- try {
- val inputStream = baseContext.contentResolver.openInputStream(it)
- val avatarBitmap = BitmapFactory.decodeStream(inputStream)
- inputStream?.close()
- val scaledBitmap = avatarBitmap.scale(192, 192)
- val exif = ExifInterface(baseContext.contentResolver.openInputStream(it)!!)
- val orientation = exif.getAttributeInt(
- ExifInterface.TAG_ORIENTATION,
- ExifInterface.ORIENTATION_NORMAL
- )
- val rotatedBitmap = rotateBitmap(scaledBitmap, orientation)
- if (tmpFile != null && tmpFile!!.exists())
- Utils.deleteFile(tmpFile!!)
- newId = System.currentTimeMillis()
- tmpFile = File(BaresipService.filesPath, "${newId}.png")
- if (Utils.saveBitmap(rotatedBitmap, tmpFile!!)) {
- imageAvatarUri.value = Uri.fromFile(tmpFile).toString()
- }
- } catch (e: Exception) {
- Log.e(TAG, "Could not read avatar image: ${e.message}")
- }
- }
- Row(
- Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) {
- Box(
- contentAlignment = Alignment.Center,
- modifier = Modifier.size(96.dp)
- .combinedClickable(
- onClick = {
- avatarRequest.launch("image/*")
- },
- onLongClick = {
- textAvatarColor.intValue = Utils.randomColor()
- color = textAvatarColor.intValue
- imageAvatarUri.value = ""
- }
- ),
- ) {
- if (imageAvatarUri.value == "") {
- textAvatarText.value = if (new)
- "?"
- else
- "${name[0]}"
- textAvatarColor.intValue = color
- TextAvatar(96)
- } else
- ImageAvatar(96)
- }
- }
- }
-
- @Composable
- fun ContactName() {
- val focusRequester = FocusRequester()
- var contactName by remember { mutableStateOf(name) }
- newName = contactName
- OutlinedTextField(
- value = contactName,
- placeholder = { Text(stringResource(R.string.contact_name)) },
- onValueChange = {
- contactName = it
- newName = contactName
- },
- modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
- textStyle = androidx.compose.ui.text.TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText
- ),
- label = { Text(stringResource(R.string.contact_name)) },
- keyboardOptions = KeyboardOptions(
- keyboardType = KeyboardType.Text,
- capitalization = KeyboardCapitalization.Words
- )
- )
- LaunchedEffect(new) {
- if (new)
- focusRequester.requestFocus()
- }
- }
-
- @Composable
- fun ContactUri() {
- var contactUri by remember { mutableStateOf(uri) }
- newUri = contactUri
- OutlinedTextField(
- value = contactUri,
- placeholder = { Text(stringResource(R.string.user_domain_or_number)) },
- onValueChange = {
- contactUri = it
- newUri = contactUri
- },
- modifier = Modifier.fillMaxWidth(),
- textStyle = androidx.compose.ui.text.TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText
- ),
- label = { Text(stringResource(R.string.sip_or_tel_uri)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
-
- @Composable
- fun Favorite() {
- Row(
- Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(
- text = stringResource(R.string.favorite),
- modifier = Modifier.weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.favorite)
- alertMessage.value = getString(R.string.favorite_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- )
- var favoriteContact by remember { mutableStateOf(favorite) }
- newFavorite = favoriteContact
- Switch(
- checked = favoriteContact,
- onCheckedChange = {
- favoriteContact = it
- newFavorite = favoriteContact
- }
- )
- }
- }
-
- @Composable
- fun Android() {
- if (new && BaresipService.contactsMode != "baresip")
- Row(
- Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(
- text = stringResource(R.string.android),
- modifier = Modifier.weight(1f),
- color = LocalCustomColors.current.itemText
- )
- var androidContact by remember { mutableStateOf(android) }
- newAndroid = androidContact
- Switch(
- checked = androidContact,
- onCheckedChange = {
- if (BaresipService.contactsMode != "android") {
- androidContact = it
- newAndroid = androidContact
- }
- }
- )
- }
- }
-
- private fun checkOnClick(ctx: Context) {
- if (BaresipService.activities.indexOf("baresip contact,$new,$uriOrName") == -1)
- return
-
- newUri = newUri.filterNot{setOf('-', ' ', '(', ')').contains(it)}
- if (!newUri.startsWith("sip:") && !newUri.startsWith("tel:"))
- newUri = if (Utils.isTelNumber(newUri))
- "tel:$newUri"
- else
- "sip:$newUri"
- if (!Utils.checkUri(newUri)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_sip_or_tel_uri), newUri)
- showAlert.value = true
- return
- }
-
- newName = newName.trim()
- if (newName == "") newName = newUri.substringAfter(":")
- if (!Utils.checkName(newName)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_contact), newName)
- showAlert.value = true
- return
- }
-
- val alert: Boolean = if (new)
- Contact.nameExists(newName, BaresipService.contacts,true)
- else {
- (uriOrName != newName) && Contact.nameExists(newName, BaresipService.contacts, false)
- }
- if (alert) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.contact_already_exists), newName)
- showAlert.value = true
- return
- }
-
- val contact: Contact.BaresipContact =
- Contact.BaresipContact(newName, newUri, color, newId, newFavorite)
-
- if (imageAvatarUri.value == "") {
- if (contact.avatarImage != null) {
- contact.avatarImage = null
- Utils.deleteFile(File(BaresipService.filesPath, "${newId}.png"))
- }
- }
- else {
- val imageFilePath = BaresipService.filesPath + "/${newId}.png"
- contact.avatarImage = BitmapFactory.decodeFile(imageFilePath)
- }
-
- if (newAndroid)
- addOrUpdateAndroidContact(ctx, contact)
- else {
- if (new)
- Contact.addBaresipContact(contact)
- else
- Contact.updateBaresipContact(id, contact)
- }
-
- BaresipService.activities.remove("baresip contact,$new,$uriOrName")
-
- val i = Intent(ctx, MainActivity::class.java)
- if (newAndroid && contact.favorite)
- i.putExtra("name", newName)
- setResult(RESULT_OK, i)
- finish()
- }
-
- private fun rotateBitmap(bitmap: Bitmap, orientation: Int): Bitmap {
- val matrix = Matrix()
- when (orientation) {
- ExifInterface.ORIENTATION_NORMAL -> return bitmap
- ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f)
- ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f)
- ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
- matrix.setRotate(180f)
- matrix.postScale(-1f, 1f)
- }
- ExifInterface.ORIENTATION_TRANSPOSE -> {
- matrix.setRotate(90f)
- matrix.postScale(-1f, 1f)
- }
- ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f)
- ExifInterface.ORIENTATION_TRANSVERSE -> {
- matrix.setRotate(-90f)
- matrix.postScale(-1f, 1f)
- }
- ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
- else -> return bitmap
- }
- val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
- bitmap.width, bitmap.height, matrix, true)
- bitmap.recycle()
- return rotatedBitmap
- }
-
- private fun addOrUpdateAndroidContact(ctx: Context, contact: Contact.BaresipContact) {
- val projection = arrayOf(ContactsContract.Data.RAW_CONTACT_ID)
- val selection = ContactsContract.Data.MIMETYPE + "='" +
- CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "' AND " +
- CommonDataKinds.StructuredName.DISPLAY_NAME + "='" + contact.name + "'"
- val c: Cursor? = ctx.contentResolver.query(
- ContactsContract.Data.CONTENT_URI, projection,
- selection, null, null)
- if (c != null && c.moveToFirst()) {
- updateAndroidContact(c.getLong(0), contact)
- } else {
- addAndroidContact(ctx, contact)
- }
- c?.close()
- }
-
- private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Boolean {
- val ops = ArrayList()
- ops.add(
- ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
- .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
- .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null).build())
- ops.add(
- ContentProviderOperation
- .newInsert(ContactsContract.Data.CONTENT_URI)
- .withValueBackReference(Data.RAW_CONTACT_ID, 0)
- .withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
- .withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, contact.name)
- .build())
- val mimeType = if (contact.uri.startsWith("sip:"))
- CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
- else
- CommonDataKinds.Phone.CONTENT_ITEM_TYPE
- ops.add(
- ContentProviderOperation
- .newInsert(ContactsContract.Data.CONTENT_URI)
- .withValueBackReference(Data.RAW_CONTACT_ID, 0)
- .withValue(Data.MIMETYPE, mimeType)
- .withValue(Data.DATA1, contact.uri.substringAfter(":"))
- .build())
-
- if (contact.avatarImage != null) {
- val photoData: ByteArray? = bitmapToPNGByteArray(contact.avatarImage!!)
- if (photoData != null) {
- ops.add(
- ContentProviderOperation
- .newInsert(ContactsContract.Data.CONTENT_URI)
- .withValueBackReference(Data.RAW_CONTACT_ID, 0)
- .withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
- .withValue(CommonDataKinds.Photo.PHOTO, photoData)
- .build())
- }
- }
- try {
- ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
- } catch (e: Exception) {
- Log.e(TAG, "Adding of contact ${contact.name} failed: ${e.message}")
- return false
- }
- return true
- }
-
- private fun updateAndroidContact(rawContactId: Long, contact: Contact.BaresipContact) {
- if (updateAndroidUri(rawContactId, contact.uri) == 0)
- addAndroidUri(rawContactId, contact.uri)
- if (updateAndroidPhoto(rawContactId, contact.avatarImage) == 0)
- if (contact.avatarImage != null)
- addAndroidPhoto(rawContactId, contact.avatarImage!!)
- }
-
- private fun addAndroidUri(rawContactId: Long, uri: String) {
- val mimeType = if (uri.startsWith("sip:"))
- CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
- else
- CommonDataKinds.Phone.CONTENT_ITEM_TYPE
- val ops = ArrayList()
- ops.add(
- ContentProviderOperation
- .newInsert(ContactsContract.Data.CONTENT_URI)
- .withValue(Data.RAW_CONTACT_ID, rawContactId)
- .withValue(Data.MIMETYPE, mimeType)
- .withValue(Data.DATA1, uri.substringAfter(":"))
- .build())
- try {
- contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
- } catch (e: Exception) {
- Log.e(TAG, "Adding of SIP URI $uri failed: ${e.message}")
- }
- }
-
- private fun updateAndroidUri(rawContactId: Long, uri: String): Int {
- val mimeType = if (uri.startsWith("sip:"))
- CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
- else
- CommonDataKinds.Phone.CONTENT_ITEM_TYPE
- val contentValues = ContentValues()
- contentValues.put(ContactsContract.Data.DATA1, uri)
- val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
- "${ContactsContract.Data.MIMETYPE}='$mimeType'"
- return try {
- contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
- } catch (e: Exception) {
- Log.e(TAG, "Update of Android URI $uri failed: ${e.message}")
- 0
- }
- }
-
- private fun addAndroidPhoto(rawContactId: Long, photoBits: Bitmap) {
- val photoBytes = bitmapToPNGByteArray(photoBits)
- if (photoBytes != null) {
- val ops = ArrayList()
- ops.add(
- ContentProviderOperation
- .newInsert(ContactsContract.Data.CONTENT_URI)
- .withValue(Data.RAW_CONTACT_ID, rawContactId)
- .withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
- .withValue(CommonDataKinds.Photo.PHOTO, photoBytes)
- .build())
- try {
- contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
- } catch (e: Exception) {
- Log.e(TAG, "Adding of Android photo failed: ${e.message}")
- }
- }
- }
-
- private fun updateAndroidPhoto(rawContactId: Long, photoBits: Bitmap?): Int {
- val photoBytes = if (photoBits == null)
- null
- else
- bitmapToPNGByteArray(photoBits)
- val contentValues = ContentValues()
- contentValues.put(CommonDataKinds.Photo.PHOTO, photoBytes)
- val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
- "${ContactsContract.Data.MIMETYPE}='${CommonDataKinds.Photo.CONTENT_ITEM_TYPE}'"
- return try {
- contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
- } catch (e: Exception) {
- Log.e(TAG, "updateAndroidPhoto failed: ${e.message}")
- 0
- }
- }
-
- private fun bitmapToPNGByteArray(bitmap: Bitmap): ByteArray? {
- val size = bitmap.width * bitmap.height * 4
- val out = ByteArrayOutputStream(size)
- return try {
- bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
- out.flush()
- out.close()
- out.toByteArray()
- } catch (e: Exception) {
- Log.w(TAG, "Unable to serialize photo: ${e.message}")
- null
- }
- }
-
- override fun onDestroy() {
- if (Build.VERSION.SDK_INT >= 33) {
- if (backInvokedCallback != null)
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
- }
- else
- onBackPressedCallback.remove()
- super.onDestroy()
- }
-
- private fun goBack() {
- BaresipService.activities.remove("baresip contact,$new,$uriOrName")
- setResult(RESULT_CANCELED, Intent(this, MainActivity::class.java))
- finish()
- }
-}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipContactScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipContactScreen.kt
new file mode 100644
index 00000000..36986dd9
--- /dev/null
+++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipContactScreen.kt
@@ -0,0 +1,819 @@
+package com.tutpro.baresip
+
+import android.content.ContentProviderOperation
+import android.content.ContentValues
+import android.content.Context
+import android.database.Cursor
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Matrix
+import android.net.Uri
+import android.provider.ContactsContract
+import android.provider.ContactsContract.CommonDataKinds
+import android.provider.ContactsContract.Contacts.Data
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.graphics.scale
+import androidx.exifinterface.media.ExifInterface
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import coil.compose.rememberAsyncImagePainter
+import com.tutpro.baresip.CustomElements.AlertDialog
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.FileOutputStream
+
+fun NavGraphBuilder.baresipContactScreenRoute(navController: NavController) {
+ composable(
+ route = "baresip_contact/{uri_or_name}/{kind}",
+ arguments = listOf(
+ navArgument("uri_or_name") { type = NavType.StringType },
+ navArgument("kind") { type = NavType.StringType }
+ )
+ ) { backStackEntry ->
+ val uriOrNameArg = backStackEntry.arguments?.getString("uri_or_name")!!
+ val kindArg = backStackEntry.arguments?.getString("kind")!!
+ ContactScreen(
+ navController = navController,
+ uriOrNameArg = uriOrNameArg,
+ kindArg = kindArg
+ )
+ }
+}
+
+private data class ScreenState(
+ val new: Boolean = false,
+ val favorite: Boolean = false,
+ val android: Boolean = false,
+ val id: Long = 0,
+ val newId: Long = 0,
+ val name: String = "",
+ val uri: String = "",
+ val color: Int = 0,
+ val avatarImageUri: String? = null,
+ val tmpAvatarFile: File? = null,
+ val isLoading: Boolean = true
+)
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun ContactScreen(
+ navController: NavController,
+ uriOrNameArg: String,
+ kindArg: String
+) {
+
+ val ctx = LocalContext.current
+
+ var screenState by remember { mutableStateOf(ScreenState()) }
+
+ val title = if (screenState.new)
+ stringResource(R.string.new_contact)
+ else
+ uriOrNameArg
+
+ LaunchedEffect(key1 = uriOrNameArg, key2 = kindArg) {
+ val isNew = kindArg == "new"
+ if (isNew) {
+ val time = System.currentTimeMillis()
+ screenState = ScreenState(
+ new = true,
+ name = "",
+ uri = uriOrNameArg,
+ favorite = false,
+ android = BaresipService.contactsMode == "android",
+ color = Utils.randomColor(),
+ id = time,
+ newId = time,
+ isLoading = false
+ )
+ }
+ else {
+ val contact = Contact.baresipContact(uriOrNameArg)!!
+ val avatarFile = File(BaresipService.filesPath, "${contact.id}.png")
+ screenState = ScreenState(
+ new = false,
+ name = uriOrNameArg,
+ uri = contact.uri,
+ favorite = contact.favorite,
+ android = false,
+ color = contact.color,
+ id = contact.id,
+ newId = contact.id,
+ avatarImageUri = if (contact.avatarImage != null && avatarFile.exists())
+ Uri.fromFile(avatarFile).toString()
+ else
+ null,
+ isLoading = false
+ )
+ }
+ }
+
+ val onBack: () -> Unit = {
+ screenState.tmpAvatarFile?.let { tempFile ->
+ if (tempFile.exists()) {
+ Log.d(TAG, "Back pressed, deleting temp avatar: ${tempFile.name}")
+ Utils.deleteFile(tempFile)
+ }
+ }
+ navController.popBackStack()
+ }
+
+ val onCheck: () -> Unit = {
+ val result = checkOnClick(
+ ctx = ctx,
+ currentState = screenState,
+ uriOrNameArg = uriOrNameArg,
+ )
+ if (result)
+ navController.popBackStack()
+ }
+
+ Scaffold(
+ modifier = Modifier
+ .fillMaxSize()
+ .imePadding(),
+ containerColor = LocalCustomColors.current.background,
+ topBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
+ ) {
+ TopAppBar(title, onBack = onBack, onCheck = onCheck)
+ }
+ },
+ content = { contentPadding ->
+ if (!screenState.isLoading) {
+ ContactContent(
+ contentPadding = contentPadding,
+ screenState = screenState,
+ onStateChange = { newState -> screenState = newState }
+ )
+ } else {
+ // Optional: Show a loading indicator
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
+ }
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TopAppBar(title: String, onBack: () -> Unit, onCheck: () -> Unit) {
+ TopAppBar(
+ title = {
+ Text(
+ text = title,
+ color = LocalCustomColors.current.light,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ colors = TopAppBarDefaults.mediumTopAppBarColors(
+ containerColor = LocalCustomColors.current.primary
+ ),
+ windowInsets = WindowInsets(0, 0, 0, 0),
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back",
+ tint = LocalCustomColors.current.light
+ )
+ }
+ },
+ actions = {
+ IconButton(onClick = onCheck) {
+ Icon(
+ imageVector = Icons.Filled.Check,
+ tint = LocalCustomColors.current.light,
+ contentDescription = "Check"
+ )
+ }
+ }
+ )
+}
+
+private val alertTitle = mutableStateOf("")
+private val alertMessage = mutableStateOf("")
+private val showAlert = mutableStateOf(false)
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun ContactContent(
+ contentPadding: PaddingValues,
+ screenState: ScreenState,
+ onStateChange: (ScreenState) -> Unit
+) {
+ val ctx = LocalContext.current
+
+ if (showAlert.value) {
+ AlertDialog(
+ showDialog = showAlert,
+ title = alertTitle.value,
+ message = alertMessage.value,
+ positiveButtonText = stringResource(R.string.ok),
+ )
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(contentPadding)
+ .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ Avatar(
+ ctx = ctx,
+ name = screenState.name,
+ color = screenState.color,
+ currentAvatarUri = screenState.avatarImageUri,
+ onNewAvatarChosen = { processedTmpFile, generatedNewId ->
+ onStateChange(
+ screenState.copy(
+ avatarImageUri = Uri.fromFile(processedTmpFile).toString(),
+ tmpAvatarFile = processedTmpFile,
+ newId = generatedNewId
+ )
+ )
+ },
+ onAvatarColorChange = { newRandomColor ->
+ // User long-clicked to change color, this means discarding any image.
+ // The old tempAvatarFile (if any) should be deleted.
+ screenState.tmpAvatarFile?.let {
+ if (it.exists())
+ Utils.deleteFile(it)
+ }
+ onStateChange(
+ screenState.copy(
+ color = newRandomColor,
+ avatarImageUri = null,
+ tmpAvatarFile = null
+ )
+ )
+ }
+ )
+ ContactName(
+ name = screenState.name,
+ new = screenState.new,
+ onNameChange = { newName -> onStateChange(screenState.copy(name = newName)) }
+ )
+ ContactUri(
+ uri = screenState.uri,
+ onUriChange = { newUri -> onStateChange(screenState.copy(uri = newUri)) }
+ )
+ Favorite(
+ ctx = ctx,
+ favorite = screenState.favorite,
+ onFavoriteChange = {
+ newFavorite -> onStateChange(screenState.copy(favorite = newFavorite))
+ }
+ )
+ if (screenState.new && BaresipService.contactsMode == "both")
+ Android(
+ android = screenState.android,
+ onAndroidChange = { newAndroid -> onStateChange(screenState.copy(android = newAndroid)) }
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun Avatar(
+ ctx: Context,
+ name: String,
+ color: Int,
+ currentAvatarUri: String?,
+ onNewAvatarChosen: (newImageFile: File, newImageId: Long) -> Unit,
+ onAvatarColorChange: (newColor: Int) -> Unit
+) {
+ val avatarImagePicker =
+ rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
+ if (uri != null) {
+ try {
+ val inputStream = ctx.contentResolver.openInputStream(uri)
+ val avatarBitmap = BitmapFactory.decodeStream(inputStream)
+ inputStream?.close()
+
+ if (avatarBitmap == null) {
+ Log.e(TAG, "Failed to decode bitmap from URI: $uri")
+ return@rememberLauncherForActivityResult
+ }
+
+ val scaledBitmap = avatarBitmap.scale(192, 192) // Define desired scale
+
+ val orientationInputStream = ctx.contentResolver.openInputStream(uri)
+ val exif = if (orientationInputStream != null) ExifInterface(orientationInputStream) else null
+ orientationInputStream?.close()
+ val orientation = exif?.getAttributeInt(
+ ExifInterface.TAG_ORIENTATION,
+ ExifInterface.ORIENTATION_NORMAL
+ ) ?: ExifInterface.ORIENTATION_NORMAL
+ val rotatedBitmap = rotateBitmap(scaledBitmap, orientation)
+
+ val newImageId = System.currentTimeMillis()
+ val tempNewImageFile = File(BaresipService.filesPath, "${newImageId}.png")
+
+ if (saveBitmap(rotatedBitmap, tempNewImageFile)) {
+ onNewAvatarChosen(tempNewImageFile, newImageId)
+ } else {
+ Log.e(TAG, "Failed to save processed avatar image to ${tempNewImageFile.absolutePath}")
+ if (tempNewImageFile.exists()) Utils.deleteFile(tempNewImageFile)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Could not process avatar image: ${e.message}")
+ }
+ }
+ }
+
+ Row(
+ Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .size(avatarSize.dp)
+ .clip(CircleShape)
+ .background(if (currentAvatarUri == null) Color(color) else Color.Transparent)
+ .combinedClickable(
+ onClick = {
+ avatarImagePicker.launch("image/*")
+ },
+ onLongClick = {
+ onAvatarColorChange(Utils.randomColor())
+ }
+ )
+ ) {
+ if (currentAvatarUri == null) {
+ Box(
+ modifier = Modifier.size(avatarSize.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Canvas(modifier = Modifier.fillMaxSize()) {
+ drawCircle(SolidColor(Color(color)))
+ }
+ val text = if (name.isNotBlank()) name.substring(0, 1).uppercase() else "?"
+ Text(text, fontSize = 72.sp, color = Color.White)
+ }
+ } else {
+ Image(
+ painter = rememberAsyncImagePainter(model = currentAvatarUri),
+ contentDescription = stringResource(R.string.avatar_image),
+ contentScale = ContentScale.Crop,
+ modifier = Modifier.size(avatarSize.dp).clip(CircleShape)
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ContactName(name: String, new: Boolean, onNameChange: (String) -> Unit) {
+ val focusRequester = FocusRequester()
+ OutlinedTextField(
+ value = name,
+ placeholder = { Text(stringResource(R.string.contact_name)) },
+ onValueChange = onNameChange,
+ modifier = Modifier
+ .fillMaxWidth()
+ .focusRequester(focusRequester),
+ textStyle = androidx.compose.ui.text.TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText
+ ),
+ label = { Text(stringResource(R.string.contact_name)) },
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ capitalization = KeyboardCapitalization.Words
+ )
+ )
+ LaunchedEffect(new) {
+ if (new)
+ focusRequester.requestFocus()
+ }
+}
+
+@Composable
+private fun ContactUri(uri: String, onUriChange: (String) -> Unit) {
+ OutlinedTextField(
+ value = uri,
+ placeholder = { Text(stringResource(R.string.user_domain_or_number)) },
+ onValueChange = onUriChange,
+ modifier = Modifier.fillMaxWidth(),
+ textStyle = androidx.compose.ui.text.TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText
+ ),
+ label = { Text(stringResource(R.string.sip_or_tel_uri)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+}
+
+@Composable
+private fun Favorite(ctx: Context, favorite: Boolean, onFavoriteChange: (Boolean) -> Unit) {
+ Row(
+ Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ Text(
+ text = stringResource(R.string.favorite),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.favorite)
+ alertMessage.value = ctx.getString(R.string.favorite_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ )
+ Switch(
+ checked = favorite,
+ onCheckedChange = onFavoriteChange
+ )
+ }
+}
+
+@Composable
+private fun Android(android: Boolean, onAndroidChange: (Boolean) -> Unit) {
+ Row(
+ Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ Text(
+ text = stringResource(R.string.android),
+ modifier = Modifier.weight(1f),
+ color = LocalCustomColors.current.itemText
+ )
+ Switch(
+ checked = android,
+ onCheckedChange = onAndroidChange
+ )
+ }
+}
+
+private fun checkOnClick(
+ ctx: Context,
+ currentState: ScreenState,
+ uriOrNameArg: String
+): Boolean {
+
+ var newUri = currentState.uri.filterNot{setOf('-', ' ', '(', ')').contains(it)}
+ if (!newUri.startsWith("sip:") && !newUri.startsWith("tel:"))
+ newUri = if (Utils.isTelNumber(newUri))
+ "tel:$newUri"
+ else
+ "sip:$newUri"
+
+ if (!Utils.checkUri(newUri)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), newUri)
+ showAlert.value = true
+ return false
+ }
+
+ var newName = currentState.name.trim()
+ if (newName == "") newName = newUri.substringAfter(":")
+ if (!Utils.checkName(newName)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_contact), newName)
+ showAlert.value = true
+ return false
+ }
+
+ val alert: Boolean = if (currentState.new)
+ Contact.nameExists(newName, BaresipService.contacts, true)
+ else {
+ (uriOrNameArg != newName) && Contact.nameExists(newName, BaresipService.contacts, false)
+ }
+ if (alert) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.contact_already_exists), newName)
+ showAlert.value = true
+ return false
+ }
+
+ var idToUse = currentState.id
+
+ if (currentState.tmpAvatarFile != null && currentState.tmpAvatarFile.exists()) {
+ if (currentState.id != currentState.newId) { // Avatar changed, implying new ID
+ // Delete old avatar if it existed for currentState.contactId
+ val oldAvatar = File(BaresipService.filesPath, "${currentState.id}.png")
+ if (oldAvatar.exists()) Utils.deleteFile(oldAvatar)
+ idToUse = currentState.newId // Use the new ID for the contact
+ }
+ /*val avatarFile = File(BaresipService.filesPath, "$idToUse.png")
+ if (!Utils.moveFile(currentState.tempAvatarFile, avatarFile)) {
+ Log.e(TAG, "Failed to move tmp avatar file $idToUse.png")
+ return "Failed to save avatar"
+ }*/
+ } else if (currentState.avatarImageUri == null) { // Avatar was explicitly cleared
+ val avatarFile = File(BaresipService.filesPath, "$idToUse.png")
+ if (avatarFile.exists())
+ Utils.deleteFile(avatarFile)
+ }
+
+ val contact: Contact.BaresipContact =
+ Contact.BaresipContact(
+ newName,
+ newUri,
+ currentState.color,
+ idToUse,
+ currentState.favorite
+ )
+
+ if (currentState.avatarImageUri == null)
+ contact.avatarImage = null
+ else {
+ val imageFilePath = BaresipService.filesPath + "/${idToUse}.png"
+ contact.avatarImage = BitmapFactory.decodeFile(imageFilePath)
+ }
+
+ if (currentState.android) {
+ addOrUpdateAndroidContact(ctx, contact)
+ if (contact.favorite) {
+ val contentValues = ContentValues()
+ contentValues.put(ContactsContract.Contacts.STARRED, 1)
+ try {
+ ctx.contentResolver.update(
+ ContactsContract.RawContacts.CONTENT_URI, contentValues,
+ ContactsContract.Contacts.DISPLAY_NAME + "='" + newName + "'", null
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "Update of Android favorite failed: ${e.message}")
+ }
+ }
+ }
+ else {
+ if (currentState.new)
+ Contact.addBaresipContact(contact)
+ else
+ Contact.updateBaresipContact(currentState.id, contact)
+ }
+
+ return true
+}
+
+private fun rotateBitmap(bitmap: Bitmap, orientation: Int): Bitmap {
+ val matrix = Matrix()
+ when (orientation) {
+ ExifInterface.ORIENTATION_NORMAL -> return bitmap
+ ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f)
+ ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f)
+ ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
+ matrix.setRotate(180f)
+ matrix.postScale(-1f, 1f)
+ }
+ ExifInterface.ORIENTATION_TRANSPOSE -> {
+ matrix.setRotate(90f)
+ matrix.postScale(-1f, 1f)
+ }
+ ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f)
+ ExifInterface.ORIENTATION_TRANSVERSE -> {
+ matrix.setRotate(-90f)
+ matrix.postScale(-1f, 1f)
+ }
+ ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
+ else -> return bitmap
+ }
+ val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
+ bitmap.width, bitmap.height, matrix, true)
+ bitmap.recycle()
+ return rotatedBitmap
+}
+
+private fun saveBitmap(bitmap: Bitmap, file: File): Boolean {
+ if (file.exists()) file.delete()
+ try {
+ val out = FileOutputStream(file)
+ val scaledBitmap = bitmap.scale(avatarSize, avatarSize)
+ scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
+ out.flush()
+ out.close()
+ Log.d(TAG, "Saved bitmap to ${file.absolutePath} of length ${file.length()}")
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to save bitmap to ${file.absolutePath}: $e")
+ return false
+ }
+ return true
+}
+
+private fun addOrUpdateAndroidContact(ctx: Context, contact: Contact.BaresipContact) {
+ val projection = arrayOf(ContactsContract.Data.RAW_CONTACT_ID)
+ val selection = ContactsContract.Data.MIMETYPE + "='" +
+ CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "' AND " +
+ CommonDataKinds.StructuredName.DISPLAY_NAME + "='" + contact.name + "'"
+ val c: Cursor? = ctx.contentResolver.query(
+ ContactsContract.Data.CONTENT_URI, projection,
+ selection, null, null)
+ if (c != null && c.moveToFirst()) {
+ updateAndroidContact(ctx, c.getLong(0), contact)
+ } else {
+ addAndroidContact(ctx, contact)
+ }
+ c?.close()
+}
+
+private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Boolean {
+ val ops = ArrayList()
+ ops.add(
+ ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null).build())
+ ops.add(
+ ContentProviderOperation
+ .newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, 0)
+ .withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, contact.name)
+ .build())
+ val mimeType = if (contact.uri.startsWith("sip:"))
+ CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
+ else
+ CommonDataKinds.Phone.CONTENT_ITEM_TYPE
+ ops.add(
+ ContentProviderOperation
+ .newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, 0)
+ .withValue(Data.MIMETYPE, mimeType)
+ .withValue(Data.DATA1, contact.uri.substringAfter(":"))
+ .build())
+
+ if (contact.avatarImage != null) {
+ val photoData: ByteArray? = bitmapToPNGByteArray(contact.avatarImage!!)
+ if (photoData != null) {
+ ops.add(
+ ContentProviderOperation
+ .newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, 0)
+ .withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
+ .withValue(CommonDataKinds.Photo.PHOTO, photoData)
+ .build())
+ }
+ }
+ try {
+ ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
+ } catch (e: Exception) {
+ Log.e(TAG, "Adding of contact ${contact.name} failed: ${e.message}")
+ return false
+ }
+ return true
+}
+
+private fun updateAndroidContact(ctx: Context, rawContactId: Long, contact: Contact.BaresipContact) {
+ if (updateAndroidUri(ctx, rawContactId, contact.uri) == 0)
+ addAndroidUri(ctx, rawContactId, contact.uri)
+ if (updateAndroidPhoto(ctx, rawContactId, contact.avatarImage) == 0)
+ if (contact.avatarImage != null)
+ addAndroidPhoto(ctx, rawContactId, contact.avatarImage!!)
+}
+
+private fun addAndroidUri(ctx: Context, rawContactId: Long, uri: String) {
+ val mimeType = if (uri.startsWith("sip:"))
+ CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
+ else
+ CommonDataKinds.Phone.CONTENT_ITEM_TYPE
+ val ops = ArrayList()
+ ops.add(
+ ContentProviderOperation
+ .newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValue(Data.RAW_CONTACT_ID, rawContactId)
+ .withValue(Data.MIMETYPE, mimeType)
+ .withValue(Data.DATA1, uri.substringAfter(":"))
+ .build())
+ try {
+ ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
+ } catch (e: Exception) {
+ Log.e(TAG, "Adding of SIP URI $uri failed: ${e.message}")
+ }
+}
+
+private fun updateAndroidUri(ctx: Context, rawContactId: Long, uri: String): Int {
+ val mimeType = if (uri.startsWith("sip:"))
+ CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
+ else
+ CommonDataKinds.Phone.CONTENT_ITEM_TYPE
+ val contentValues = ContentValues()
+ contentValues.put(ContactsContract.Data.DATA1, uri)
+ val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
+ "${ContactsContract.Data.MIMETYPE}='$mimeType'"
+ return try {
+ ctx.contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
+ } catch (e: Exception) {
+ Log.e(TAG, "Update of Android URI $uri failed: ${e.message}")
+ 0
+ }
+}
+
+private fun addAndroidPhoto(ctx: Context, rawContactId: Long, photoBits: Bitmap) {
+ val photoBytes = bitmapToPNGByteArray(photoBits)
+ if (photoBytes != null) {
+ val ops = ArrayList()
+ ops.add(
+ ContentProviderOperation
+ .newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValue(Data.RAW_CONTACT_ID, rawContactId)
+ .withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
+ .withValue(CommonDataKinds.Photo.PHOTO, photoBytes)
+ .build())
+ try {
+ ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
+ } catch (e: Exception) {
+ Log.e(TAG, "Adding of Android photo failed: ${e.message}")
+ }
+ }
+}
+
+private fun updateAndroidPhoto(ctx: Context, rawContactId: Long, photoBits: Bitmap?): Int {
+ val photoBytes = if (photoBits == null)
+ null
+ else
+ bitmapToPNGByteArray(photoBits)
+ val contentValues = ContentValues()
+ contentValues.put(CommonDataKinds.Photo.PHOTO, photoBytes)
+ val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
+ "${ContactsContract.Data.MIMETYPE}='${CommonDataKinds.Photo.CONTENT_ITEM_TYPE}'"
+ return try {
+ ctx.contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
+ } catch (e: Exception) {
+ Log.e(TAG, "updateAndroidPhoto failed: ${e.message}")
+ 0
+ }
+}
+
+private fun bitmapToPNGByteArray(bitmap: Bitmap): ByteArray? {
+ val size = bitmap.width * bitmap.height * 4
+ val out = ByteArrayOutputStream(size)
+ return try {
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
+ out.flush()
+ out.close()
+ out.toByteArray()
+ } catch (e: Exception) {
+ Log.w(TAG, "Unable to serialize photo: ${e.message}")
+ null
+ }
+}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt
index 01bf3797..f7eea096 100644
--- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt
+++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt
@@ -49,21 +49,19 @@ import android.provider.ContactsContract
import android.provider.Settings
import android.system.OsConstants
import android.telecom.TelecomManager
-import android.text.Spannable
-import android.text.SpannableString
-import android.text.style.ForegroundColorSpan
import android.view.View
import android.widget.RemoteViews
import android.widget.Toast
-import androidx.annotation.ColorRes
import androidx.annotation.Keep
-import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.MessagingStyle
+import androidx.core.app.Person
+import androidx.core.app.RemoteInput
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
@@ -553,8 +551,13 @@ class BaresipService: Service() {
val ua = UserAgent.ofUap(uap)
if (ua == null)
Log.w(TAG, "onStartCommand did not find UA $uap")
- else
- Message.updateAorMessage(ua.account.aor, intent.getStringExtra("time")!!.toLong())
+ else {
+ Message.updateAorMessage(
+ ua.account.aor,
+ intent.getStringExtra("time")!!.toLong()
+ )
+ ua.account.unreadMessages = Message.unreadMessages(ua.account.aor)
+ }
nm.cancel(MESSAGE_NOTIFICATION_ID)
}
@@ -563,9 +566,50 @@ class BaresipService: Service() {
val ua = UserAgent.ofUap(uap)
if (ua == null)
Log.w(TAG, "onStartCommand did not find UA $uap")
- else
- Message.deleteAorMessage(ua.account.aor, intent.getStringExtra("time")!!.toLong())
+ else {
+ Message.deleteAorMessage(
+ ua.account.aor,
+ intent.getStringExtra("time")!!.toLong()
+ )
+ ua.account.unreadMessages = Message.unreadMessages(ua.account.aor)
+ }
+ nm.cancel(MESSAGE_NOTIFICATION_ID)
+ }
+ "Message Inline Reply" -> {
+ val remoteInputResults = RemoteInput.getResultsFromIntent(intent!!)
+ if (remoteInputResults != null) {
+ val replyText = remoteInputResults.getCharSequence(KEY_TEXT_REPLY)?.toString()
+ if (!replyText.isNullOrEmpty()) {
+ val uap = intent.getLongExtra("uap", -1L)
+ val ua = UserAgent.ofUap(uap)!!
+ val aor = ua.account.aor
+ var peerUri = intent.getStringExtra("peer")!!
+ val timeStamp = intent.getLongExtra("time", 0L)
+ if (Utils.isTelUri(peerUri)) {
+ if (ua.account.telProvider == "") {
+ Log.w(TAG, "No telephony provider for $aor")
+ peerUri = ""
+ } else
+ peerUri = Utils.telToSip(peerUri, ua.account)
+ }
+ if (peerUri != "") {
+ Log.d(TAG, "Direct Reply from $aor to $peerUri: $replyText")
+ Message.updateAorMessage(aor, timeStamp)
+ val time = System.currentTimeMillis()
+ val msg = Message(aor, peerUri, replyText, time, MESSAGE_UP_WAIT, 0, "", false)
+ msg.add()
+ if (Api.message_send(uap, peerUri, replyText, time.toString()) != 0) {
+ Log.w(TAG, "message_send failed")
+ msg.direction = MESSAGE_UP_FAIL
+ msg.responseReason = getString(R.string.message_failed)
+ }
+ else {
+ ua.account.unreadMessages = Message.unreadMessages(aor)
+ }
+ }
+ }
+ }
nm.cancel(MESSAGE_NOTIFICATION_ID)
}
@@ -844,15 +888,15 @@ class BaresipService: Service() {
if (!Utils.isVisible()) {
val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
val intent = Intent(applicationContext, MainActivity::class.java)
+ .putExtra("action", "call show")
+ .putExtra("callp", callp)
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_NEW_TASK
- intent.putExtra("action", "call show")
- .putExtra("callp", callp)
- val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent,
- piFlags)
+ val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent, piFlags)
val nb = NotificationCompat.Builder(this,
if (shouldVibrate()) MEDIUM_CHANNEL_ID else HIGH_CHANNEL_ID)
val caller = Utils.friendlyUri(this, peerUri, ua.account)
+ val person = Person.Builder().setName(caller).build()
nb.setSmallIcon(R.drawable.ic_stat_call)
.setColor(ContextCompat.getColor(this, R.color.colorBaresip))
.setContentIntent(pi)
@@ -867,19 +911,15 @@ class BaresipService: Service() {
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setFullScreenIntent(pi, true)
val answerIntent = Intent(applicationContext, MainActivity::class.java)
- answerIntent.putExtra("action", "call answer")
+ .putExtra("action", "call answer")
.putExtra("callp", callp)
val api = PendingIntent.getActivity(applicationContext, ANSWER_REQ_CODE,
answerIntent, piFlags)
val rejectIntent = Intent(this, BaresipService::class.java)
rejectIntent.action = "Call Reject"
rejectIntent.putExtra("callp", callp)
- val rpi = PendingIntent.getService(this, REJECT_REQ_CODE,
- rejectIntent, piFlags)
- nb.addAction(R.drawable.ic_stat_call,
- getActionText(R.string.answer, R.color.colorGreen), api)
- nb.addAction(R.drawable.ic_stat_call_end,
- getActionText(R.string.reject, R.color.colorRed), rpi)
+ val rpi = PendingIntent.getService(this, REJECT_REQ_CODE, rejectIntent, piFlags)
+ nb.setStyle(NotificationCompat.CallStyle.forIncomingCall(person, rpi, api))
nm.notify(CALL_NOTIFICATION_ID, nb.build())
return
}
@@ -938,8 +978,7 @@ class BaresipService: Service() {
Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
intent.putExtra("action", "transfer show")
.putExtra("callp", callp).putExtra("uri", ev[1])
- val pi = PendingIntent.getActivity(applicationContext, TRANSFER_REQ_CODE,
- intent, piFlags)
+ val pi = PendingIntent.getActivity(applicationContext, TRANSFER_REQ_CODE, intent, piFlags)
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
val target = Utils.friendlyUri(this, ev[1], ua.account)
nb.setSmallIcon(R.drawable.ic_stat_call)
@@ -952,7 +991,7 @@ class BaresipService: Service() {
val acceptIntent = Intent(applicationContext, MainActivity::class.java)
acceptIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or
Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
- acceptIntent.putExtra("action","transfer accept")
+ acceptIntent.putExtra("action", "transfer accept")
.putExtra("callp", callp).putExtra("uri", ev[1])
val acceptPendingIntent = PendingIntent.getActivity(applicationContext,
ACCEPT_REQ_CODE, acceptIntent, piFlags)
@@ -1078,16 +1117,6 @@ class BaresipService: Service() {
}
- private fun postServiceEvent(event: ServiceEvent) {
- serviceEvents.add(event)
- if (serviceEvents.size == 1) {
- Log.d(TAG, "Posted service event ${event.event} at ${event.timeStamp}")
- serviceEvent.postValue(Event(event.timeStamp))
- } else {
- Log.d(TAG, "Added service event ${event.event}")
- }
- }
-
@Suppress("unused")
@SuppressLint("UnspecifiedImmutableFlag")
@Keep
@@ -1120,45 +1149,88 @@ class BaresipService: Service() {
ua.account.unreadMessages = true
if (!Utils.isVisible()) {
+
+ // common flags
val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+
+ // message show
val intent = Intent(applicationContext, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_NEW_TASK
- intent.putExtra("action", "message show").putExtra("uap", uap)
- .putExtra("peer", peerUri)
+ intent.putExtra("action", "message show").putExtra("uap", uap).putExtra("peer", peerUri)
val pi = PendingIntent.getActivity(applicationContext, MESSAGE_REQ_CODE, intent, piFlags)
+
+ // message notification builder
+ val senderDisplayName = Utils.friendlyUri(this, peerUri, ua.account)
+ val senderPerson = Person.Builder()
+ .setName(senderDisplayName)
+ .setKey(peerUri)
+ .build()
+ val localUserPerson = Person.Builder()
+ .setName(getString(R.string.you))
+ .setKey(ua.account.aor)
+ .build()
+ val messagingStyle = MessagingStyle(localUserPerson)
+ .setConversationTitle(null)
+ .setGroupConversation(false)
+ .addMessage(text, timeStamp, senderPerson)
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
- val sender = Utils.friendlyUri(this, peerUri, ua.account)
- nb.setSmallIcon(R.drawable.ic_stat_message)
+ .setSmallIcon(R.drawable.ic_stat_message)
.setColor(ContextCompat.getColor(this, R.color.colorBaresip))
.setContentIntent(pi)
.setSound(Settings.System.DEFAULT_NOTIFICATION_URI)
.setAutoCancel(true)
- .setContentTitle(getString(R.string.message_from) + " " + sender)
- .setContentText(text)
- val replyIntent = Intent(applicationContext, MainActivity::class.java)
- replyIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or
- Intent.FLAG_ACTIVITY_NEW_TASK
- replyIntent.putExtra("action", "message reply")
- .putExtra("uap", uap).putExtra("peer", peerUri)
- val rpi = PendingIntent.getActivity(applicationContext, REPLY_REQ_CODE, replyIntent,
- piFlags)
+ .setStyle(messagingStyle)
+ .setCategory(NotificationCompat.CATEGORY_MESSAGE)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+
+ // messafe inline reply
+ val remoteInput = RemoteInput.Builder(KEY_TEXT_REPLY)
+ .setLabel(getString(R.string.reply))
+ .build()
+ val directReplyIntent = Intent(this, BaresipService::class.java)
+ directReplyIntent.action = "Message Inline Reply"
+ directReplyIntent.putExtra("uap", uap).putExtra("peer", peerUri).putExtra("time", timeStamp)
+ val directReplyPendingIntent = PendingIntent.getService(
+ this,
+ DIRECT_REPLY_REQ_CODE,
+ directReplyIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ )
+ val inlineReplyAction = NotificationCompat.Action.Builder(
+ R.drawable.ic_stat_reply,
+ getString(R.string.reply),
+ directReplyPendingIntent
+ ).addRemoteInput(remoteInput)
+ .setAllowGeneratedReplies(true)
+ .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY).
+ build()
+
+ // message save
val saveIntent = Intent(this, BaresipService::class.java)
saveIntent.action = "Message Save"
- saveIntent.putExtra("uap", uap)
- .putExtra("time", timeStampString)
- val savePendingIntent = PendingIntent.getService(this, SAVE_REQ_CODE, saveIntent,
- piFlags)
+ saveIntent.putExtra("uap", uap).putExtra("time", timeStampString)
+ val savePendingIntent = PendingIntent.getService(this, SAVE_REQ_CODE, saveIntent, piFlags)
+ val saveAction = NotificationCompat.Action.Builder(
+ R.drawable.ic_stat_save,
+ getString(R.string.save),
+ savePendingIntent
+ ).build()
+
+ // message delete
val deleteIntent = Intent(this, BaresipService::class.java)
deleteIntent.action = "Message Delete"
- deleteIntent.putExtra("uap", uap)
- .putExtra("time", timeStampString)
- val deletePendingIntent = PendingIntent.getService(this, DELETE_REQ_CODE,
- deleteIntent, piFlags)
- nb.addAction(R.drawable.ic_stat_reply, "Reply", rpi)
- nb.addAction(R.drawable.ic_stat_save, "Save", savePendingIntent)
- nb.addAction(R.drawable.ic_stat_delete, "Delete", deletePendingIntent)
+ deleteIntent.putExtra("uap", uap).putExtra("time", timeStampString)
+ val deletePendingIntent = PendingIntent.getService(this, DELETE_REQ_CODE, deleteIntent, piFlags)
+ val deleteAction = NotificationCompat.Action.Builder(
+ R.drawable.ic_stat_delete,
+ getString(R.string.delete),
+ deletePendingIntent
+ ).build()
+
+ nb.addAction(inlineReplyAction).addAction(saveAction).addAction(deleteAction)
nm.notify(MESSAGE_NOTIFICATION_ID, nb.build())
+
return
}
@@ -1297,13 +1369,6 @@ class BaresipService: Service() {
}
}
- private fun getActionText(@StringRes stringRes: Int, @ColorRes colorRes: Int): Spannable {
- val spannable: Spannable = SpannableString(applicationContext.getText(stringRes))
- spannable.setSpan(
- ForegroundColorSpan(applicationContext.getColor(colorRes)), 0, spannable.length, 0)
- return spannable
- }
-
private fun startRinging() {
am.mode = AudioManager.MODE_RINGTONE
rt!!.isLooping = true
@@ -1638,12 +1703,9 @@ class BaresipService: Service() {
val baresipContacts = mutableStateOf(emptyList())
val androidContacts = mutableStateOf(emptyList())
val contactNames = mutableStateOf(emptyList())
- val contactUpdate = MutableLiveData()
val darkTheme = mutableStateOf(false)
var messages by mutableStateOf(emptyList())
val messageUpdate = MutableLiveData()
- val chatTexts: MutableMap = mutableMapOf()
- val activities = mutableListOf()
val registrationUpdate = MutableLiveData()
val serviceEvent = MutableLiveData>()
val serviceEvents = mutableListOf()
@@ -1661,12 +1723,25 @@ class BaresipService: Service() {
private var aec: AcousticEchoCanceler? = null
var agcAvailable = false
var rt: Ringtone? = null
+
private var agc: AutomaticGainControl? = null
private val nsAvailable = NoiseSuppressor.isAvailable()
private var ns: NoiseSuppressor? = null
private var btAdapter: BluetoothAdapter? = null
private var recorderSessionId = 0
+ internal const val KEY_TEXT_REPLY = "key_text_reply_baresip"
+
+ fun postServiceEvent(event: ServiceEvent) {
+ serviceEvents.add(event)
+ if (serviceEvents.size == 1) {
+ Log.d(TAG, "Posted service event ${event.event} at ${event.timeStamp}")
+ serviceEvent.postValue(Event(event.timeStamp))
+ } else {
+ Log.d(TAG, "Added service event ${event.event}")
+ }
+ }
+
fun requestAudioFocus(ctx: Context): Boolean {
Log.d(TAG, "Requesting audio focus")
if (audioFocusRequest != null) {
diff --git a/app/src/main/kotlin/com/tutpro/baresip/CallDetailsActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/CallDetailsActivity.kt
deleted file mode 100644
index b5a66383..00000000
--- a/app/src/main/kotlin/com/tutpro/baresip/CallDetailsActivity.kt
+++ /dev/null
@@ -1,404 +0,0 @@
-package com.tutpro.baresip
-
-import android.content.Context
-import android.media.AudioAttributes
-import android.media.MediaPlayer
-import android.os.Build
-import android.os.Bundle
-import android.text.format.DateUtils
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.core.net.toUri
-import com.tutpro.baresip.CallsActivity.Companion.uaHistory
-import com.tutpro.baresip.CustomElements.AlertDialog
-import com.tutpro.baresip.CustomElements.verticalScrollbar
-import java.io.File
-import java.io.IOException
-import java.text.DateFormat
-import java.util.GregorianCalendar
-
-class CallDetailsActivity : ComponentActivity() {
-
- private lateinit var aor: String
- private lateinit var peer: String
- private lateinit var account: Account
- private lateinit var callRow: CallRow
- private lateinit var details: ArrayList
- private val decPlayer = MediaPlayer()
- private val encPlayer = MediaPlayer()
- private var position = 0
-
- private var backInvokedCallback: OnBackInvokedCallback? = null
- private lateinit var onBackPressedCallback: OnBackPressedCallback
-
- @RequiresApi(33)
- private fun registerBackInvokedCallback() {
- backInvokedCallback = OnBackInvokedCallback { goBack() }
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- backInvokedCallback!!
- )
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
-
- super.onCreate(savedInstanceState)
-
- enableEdgeToEdge()
-
- if (Build.VERSION.SDK_INT >= 33)
- registerBackInvokedCallback()
- else {
- onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- goBack()
- }
- }
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
- aor = intent.getStringExtra("aor")!!
- account = Account.ofAor(aor)!!
- peer = intent.getStringExtra("peer")!!
- position = intent.getIntExtra("position", 0)
- callRow = uaHistory.value[position]
- details = callRow.details
-
- Utils.addActivity("call_details,$aor,$peer,$position")
-
- setContent {
- AppTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = LocalCustomColors.current.background
- ) {
- CallDetailsScreen(this, stringResource(R.string.call_details)) { goBack() }
- }
- }
- }
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- fun CallDetailsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
- Scaffold(
- modifier = Modifier
- .fillMaxHeight()
- .imePadding()
- .safeDrawingPadding(),
- containerColor = LocalCustomColors.current.background,
- topBar = {
- TopAppBar(
- title = {
- Text(
- text = title,
- color = LocalCustomColors.current.light,
- fontWeight = FontWeight.Bold
- )
- },
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = LocalCustomColors.current.primary
- ),
- navigationIcon = {
- IconButton(onClick = navigateBack) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = LocalCustomColors.current.light
- )
- }
- },
- )
- },
- content = { contentPadding ->
- CallDetailsContent(ctx, contentPadding)
- }
- )
- }
-
- @Composable
- fun CallDetailsContent(ctx: Context, contentPadding: PaddingValues) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(contentPadding)
- .padding(top = 16.dp, start = 16.dp, end = 4.dp, bottom = 16.dp),
- verticalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- Peer(ctx, peer, account)
- Calls(ctx)
- }
- }
-
- @Composable
- fun Peer(ctx: Context, peer: String, account: Account) {
- val headerText = stringResource(R.string.peer) + " " +
- Utils.friendlyUri(ctx, peer, account)
- Text(
- text = headerText,
- modifier = Modifier.fillMaxWidth(),
- fontSize = 18.sp,
- fontWeight = FontWeight.SemiBold,
- color = LocalCustomColors.current.itemText,
- textAlign = TextAlign.Center
- )
- }
-
- @Composable
- fun Calls(ctx: Context) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(text = stringResource(R.string.direction),
- color = LocalCustomColors.current.itemText,
- fontSize = 16.sp,
- fontWeight = FontWeight.SemiBold,
- modifier = Modifier.width(96.dp)
- )
- Spacer(modifier = Modifier.width(6.dp))
- Text(text = stringResource(R.string.time),
- color = LocalCustomColors.current.itemText,
- fontSize = 16.sp,
- fontWeight = FontWeight.SemiBold
- )
- Spacer(modifier = Modifier.weight(1f))
- Text(text = stringResource(R.string.calls_duration),
- modifier = Modifier.padding(end = 12.dp),
- color = LocalCustomColors.current.itemText,
- fontSize = 16.sp,
- fontWeight = FontWeight.SemiBold
- )
- }
- val lazyListState = rememberLazyListState()
- LazyColumn(
- modifier = Modifier
- .imePadding()
- .fillMaxWidth()
- .verticalScrollbar(
- state = lazyListState,
- width = 4.dp,
- color = LocalCustomColors.current.gray
- )
- .background(LocalCustomColors.current.background),
- state = lazyListState,
- verticalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- items(details) { detail ->
- Row(verticalAlignment = Alignment.CenterVertically) {
- Image(
- painter = painterResource(detail.direction),
- contentDescription = "Direction",
- modifier = Modifier.width(64.dp)
- )
- Spacer(modifier = Modifier.width(38.dp))
- val durationText = startTime(detail)
- Spacer(modifier = Modifier.weight(1f))
- Duration(ctx, detail, durationText)
- }
- }
- }
- }
-
- @Composable
- fun startTime(detail: CallRow.Details): String {
- val startTime = detail.startTime
- val stopTime = detail.stopTime
- val startTimeText: String
- val durationText: String
- val stopText = if (DateUtils.isToday(stopTime.timeInMillis)) {
- val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
- stringResource(R.string.today) + " " + fmt.format(stopTime.time)
- } else {
- val fmt = DateFormat.getDateTimeInstance()
- fmt.format(stopTime.time)
- }
- if (startTime == GregorianCalendar(0, 0, 0)) {
- startTimeText = stopText
- durationText = "?"
- } else {
- if (startTime == null) {
- startTimeText = stopText
- durationText = ""
- } else {
- val startText = if (DateUtils.isToday(startTime.timeInMillis)) {
- val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
- stringResource(R.string.today) + " " + fmt.format(startTime.time)
- } else {
- val fmt = DateFormat.getDateTimeInstance()
- fmt.format(startTime.time)
- }
- startTimeText = startText
- val duration = (stopTime.time.time - startTime.time.time) / 1000
- durationText = DateUtils.formatElapsedTime(duration)
- }
- }
- Text(text = startTimeText, color = LocalCustomColors.current.itemText)
- return durationText
- }
-
- @Composable
- fun Duration(ctx: Context, detail: CallRow.Details, durationText: String) {
-
- val showDialog = remember { mutableStateOf(false) }
- val recording = detail.recording
-
- AlertDialog(
- showDialog = showDialog,
- title = stringResource(R.string.playing_recording),
- message = "",
- )
-
- if (recording[0] != "") {
- Text(
- text = durationText,
- color = LocalCustomColors.current.accent,
- modifier = Modifier.padding(end = 12.dp)
- .clickable(onClick = {
- if (!decPlayer.isPlaying && !encPlayer.isPlaying) {
- decPlayer.reset()
- encPlayer.reset()
- Log.d(TAG, "Playing recordings ${recording[0]} and ${recording[1]}")
- decPlayer.apply {
- setAudioAttributes(
- AudioAttributes.Builder()
- .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
- .setUsage(AudioAttributes.USAGE_MEDIA)
- .build()
- )
- setOnPreparedListener {
- encPlayer.apply {
- setAudioAttributes(
- AudioAttributes.Builder()
- .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
- .setUsage(AudioAttributes.USAGE_MEDIA)
- .build()
- )
- setOnPreparedListener {
- it.start()
- decPlayer.start()
- Log.d(TAG, "Started players")
- showDialog.value = true
- }
- setOnCompletionListener {
- Log.d(TAG, "Stopping encPlayer")
- it.stop()
- showDialog.value = false
- }
- try {
- val file = recording[0]
- val encFile = File(file)
- .copyTo(File(BaresipService.filesPath +
- "/tmp/encode.wav"), true)
- val encUri = encFile.toUri()
- setDataSource(ctx, encUri)
- prepareAsync()
- } catch (e: IllegalArgumentException) {
- Log.e(TAG, "encPlayer IllegalArgumentException: $e")
- } catch (e: IOException) {
- Log.e(TAG, "encPlayer IOException: $e")
- } catch (e: Exception) {
- Log.e(TAG, "encPlayer Exception: $e")
- }
- }
- }
- setOnCompletionListener {
- Log.d(TAG, "Stopping decPlayer")
- it.stop()
- showDialog.value = false
- }
- try {
- val file = recording[1]
- val decFile = File(file)
- .copyTo(File(BaresipService.filesPath +
- "/tmp/decode.wav"), true)
- val decUri = decFile.toUri()
- setDataSource(ctx, decUri)
- prepareAsync()
- } catch (e: IllegalArgumentException) {
- Log.e(TAG, "decPlayer IllegalArgumentException: $e")
- } catch (e: IOException) {
- Log.e(TAG, "decPlayer IOException: $e")
- } catch (e: Exception) {
- Log.e(TAG, "decPlayer Exception: $e")
- }
- }
- } else if (decPlayer.isPlaying && encPlayer.isPlaying) {
- decPlayer.stop()
- encPlayer.stop()
- }
- })
- )
- } else {
- Text(text = durationText,
- modifier = Modifier.padding(end = 12.dp),
- color = LocalCustomColors.current.itemText)
- }
- }
-
- override fun onPause() {
- MainActivity.activityAor = aor
- super.onPause()
- }
-
- override fun onDestroy() {
- if (Build.VERSION.SDK_INT >= 33) {
- if (backInvokedCallback != null)
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
- }
- else
- onBackPressedCallback.remove()
- super.onDestroy()
- }
-
- private fun goBack() {
- BaresipService.activities.remove("call_details,$aor,$peer,$position")
- decPlayer.stop()
- decPlayer.release()
- encPlayer.stop()
- encPlayer.release()
- finish()
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/tutpro/baresip/CallDetailsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/CallDetailsScreen.kt
new file mode 100644
index 00000000..93c8a7af
--- /dev/null
+++ b/app/src/main/kotlin/com/tutpro/baresip/CallDetailsScreen.kt
@@ -0,0 +1,326 @@
+package com.tutpro.baresip
+
+import android.content.Context
+import android.media.AudioAttributes
+import android.media.MediaPlayer
+import android.text.format.DateUtils
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.net.toUri
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import com.tutpro.baresip.CallRow.Details
+import com.tutpro.baresip.CustomElements.AlertDialog
+import com.tutpro.baresip.CustomElements.verticalScrollbar
+import java.io.File
+import java.io.IOException
+import java.text.DateFormat
+import java.util.GregorianCalendar
+
+fun NavGraphBuilder.callDetailsScreenRoute(navController: NavController, viewModel: ViewModel) {
+ composable("call_details") { backStackEntry ->
+ val callRow = remember { viewModel.consumeSelectedCallRow() }
+ CallDetailsScreen(navController, callRow!!)
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun CallDetailsScreen(navController: NavController, callRow: CallRow) {
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize().imePadding(),
+ containerColor = LocalCustomColors.current.background,
+ topBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
+ ) {
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(R.string.call_details),
+ color = LocalCustomColors.current.light,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ colors = TopAppBarDefaults.mediumTopAppBarColors(
+ containerColor = LocalCustomColors.current.primary
+ ),
+ windowInsets = WindowInsets(0, 0, 0, 0),
+ navigationIcon = {
+ IconButton(onClick = navController::popBackStack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back",
+ tint = LocalCustomColors.current.light
+ )
+ }
+ },
+ )
+ }
+ },
+ content = { contentPadding ->
+ CallDetailsContent(LocalContext.current, contentPadding, callRow)
+ },
+ )
+}
+
+@Composable
+private fun CallDetailsContent(ctx: Context, contentPadding: PaddingValues, callRow: CallRow) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(contentPadding)
+ .padding(top = 16.dp, start = 16.dp, end = 4.dp, bottom = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Peer(ctx, callRow)
+ Details(ctx, callRow.details)
+ }
+}
+
+@Composable
+private fun Peer(ctx: Context, callRow: CallRow) {
+ val account = Account.ofAor(callRow.aor)!!
+ val headerText = stringResource(R.string.peer) + " " + Utils.friendlyUri(ctx, callRow.peerUri, account)
+ Text(
+ text = headerText,
+ modifier = Modifier.fillMaxWidth(),
+ fontSize = 18.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = LocalCustomColors.current.itemText,
+ textAlign = TextAlign.Center
+ )
+}
+
+@Composable
+private fun Details(ctx: Context, details: ArrayList) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(text = stringResource(R.string.direction),
+ color = LocalCustomColors.current.itemText,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.width(96.dp)
+ )
+ Spacer(modifier = Modifier.width(6.dp))
+ Text(text = stringResource(R.string.time),
+ color = LocalCustomColors.current.itemText,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.SemiBold
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Text(text = stringResource(R.string.calls_duration),
+ modifier = Modifier.padding(end = 12.dp),
+ color = LocalCustomColors.current.itemText,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.SemiBold
+ )
+ }
+ val lazyListState = rememberLazyListState()
+ LazyColumn(
+ modifier = Modifier.fillMaxWidth()
+ .verticalScrollbar(
+ state = lazyListState,
+ width = 4.dp,
+ color = LocalCustomColors.current.gray
+ )
+ .background(LocalCustomColors.current.background),
+ state = lazyListState,
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ items(details) { detail ->
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Image(
+ painter = painterResource(detail.direction),
+ contentDescription = "Direction",
+ modifier = Modifier.width(64.dp)
+ )
+ Spacer(modifier = Modifier.width(38.dp))
+ val durationText = startTime(detail)
+ Spacer(modifier = Modifier.weight(1f))
+ Duration(ctx, detail, durationText)
+ }
+ }
+ }
+}
+
+@Composable
+private fun startTime(detail: Details): String {
+ val startTime = detail.startTime
+ val stopTime = detail.stopTime
+ val startTimeText: String
+ val durationText: String
+ val stopText = if (DateUtils.isToday(stopTime.timeInMillis)) {
+ val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
+ stringResource(R.string.today) + " " + fmt.format(stopTime.time)
+ } else {
+ val fmt = DateFormat.getDateTimeInstance()
+ fmt.format(stopTime.time)
+ }
+ if (startTime == GregorianCalendar(0, 0, 0)) {
+ startTimeText = stopText
+ durationText = "?"
+ } else {
+ if (startTime == null) {
+ startTimeText = stopText
+ durationText = ""
+ } else {
+ val startText = if (DateUtils.isToday(startTime.timeInMillis)) {
+ val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
+ stringResource(R.string.today) + " " + fmt.format(startTime.time)
+ } else {
+ val fmt = DateFormat.getDateTimeInstance()
+ fmt.format(startTime.time)
+ }
+ startTimeText = startText
+ val duration = (stopTime.time.time - startTime.time.time) / 1000
+ durationText = DateUtils.formatElapsedTime(duration)
+ }
+ }
+ Text(text = startTimeText, color = LocalCustomColors.current.itemText)
+ return durationText
+}
+
+@Composable
+private fun Duration(ctx: Context, detail: Details, durationText: String) {
+
+ val showDialog = remember { mutableStateOf(false) }
+ val recording = detail.recording
+ val decPlayer = MediaPlayer()
+ val encPlayer = MediaPlayer()
+
+ AlertDialog(
+ showDialog = showDialog,
+ title = stringResource(R.string.playing_recording),
+ message = "",
+ )
+
+ if (recording[0] != "") {
+ Text(
+ text = durationText,
+ color = LocalCustomColors.current.accent,
+ modifier = Modifier.padding(end = 12.dp)
+ .clickable(onClick = {
+ if (!decPlayer.isPlaying && !encPlayer.isPlaying) {
+ decPlayer.reset()
+ encPlayer.reset()
+ Log.d(TAG, "Playing recordings ${recording[0]} and ${recording[1]}")
+ decPlayer.apply {
+ setAudioAttributes(
+ AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build()
+ )
+ setOnPreparedListener {
+ encPlayer.apply {
+ setAudioAttributes(
+ AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build()
+ )
+ setOnPreparedListener {
+ it.start()
+ decPlayer.start()
+ Log.d(TAG, "Started players")
+ showDialog.value = true
+ }
+ setOnCompletionListener {
+ Log.d(TAG, "Stopping encPlayer")
+ it.stop()
+ showDialog.value = false
+ }
+ try {
+ val file = recording[0]
+ val encFile = File(file)
+ .copyTo(File(BaresipService.filesPath +
+ "/tmp/encode.wav"), true)
+ val encUri = encFile.toUri()
+ setDataSource(ctx, encUri)
+ prepareAsync()
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG, "encPlayer IllegalArgumentException: $e")
+ } catch (e: IOException) {
+ Log.e(TAG, "encPlayer IOException: $e")
+ } catch (e: Exception) {
+ Log.e(TAG, "encPlayer Exception: $e")
+ }
+ }
+ }
+ setOnCompletionListener {
+ Log.d(TAG, "Stopping decPlayer")
+ it.stop()
+ showDialog.value = false
+ }
+ try {
+ val file = recording[1]
+ val decFile = File(file)
+ .copyTo(File(BaresipService.filesPath +
+ "/tmp/decode.wav"), true)
+ val decUri = decFile.toUri()
+ setDataSource(ctx, decUri)
+ prepareAsync()
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG, "decPlayer IllegalArgumentException: $e")
+ } catch (e: IOException) {
+ Log.e(TAG, "decPlayer IOException: $e")
+ } catch (e: Exception) {
+ Log.e(TAG, "decPlayer Exception: $e")
+ }
+ }
+ } else if (decPlayer.isPlaying && encPlayer.isPlaying) {
+ decPlayer.stop()
+ encPlayer.stop()
+ }
+ })
+ )
+ } else {
+ Text(text = durationText,
+ modifier = Modifier.padding(end = 12.dp),
+ color = LocalCustomColors.current.itemText)
+ }
+}
+
diff --git a/app/src/main/kotlin/com/tutpro/baresip/CallRow.kt b/app/src/main/kotlin/com/tutpro/baresip/CallRow.kt
index 45d849a2..b2aaf5a7 100644
--- a/app/src/main/kotlin/com/tutpro/baresip/CallRow.kt
+++ b/app/src/main/kotlin/com/tutpro/baresip/CallRow.kt
@@ -5,9 +5,7 @@ import java.util.*
class CallRow(
val aor: String, val peerUri: String, val direction: Int, startTime: GregorianCalendar?,
val stopTime: GregorianCalendar, val recording: Array
-)
-{
-
+) {
class Details(
val direction: Int, val startTime: GregorianCalendar?,
val stopTime: GregorianCalendar, val recording: Array
diff --git a/app/src/main/kotlin/com/tutpro/baresip/CallsActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/CallsActivity.kt
deleted file mode 100644
index c5f2c8a4..00000000
--- a/app/src/main/kotlin/com/tutpro/baresip/CallsActivity.kt
+++ /dev/null
@@ -1,545 +0,0 @@
-package com.tutpro.baresip
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.Intent
-import android.graphics.BitmapFactory
-import android.os.Build
-import android.os.Bundle
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.Menu
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import coil.compose.AsyncImage
-import com.tutpro.baresip.CustomElements.AlertDialog
-import com.tutpro.baresip.CustomElements.ImageAvatar
-import com.tutpro.baresip.CustomElements.TextAvatar
-import com.tutpro.baresip.CustomElements.verticalScrollbar
-
-class CallsActivity : ComponentActivity() {
-
- private lateinit var account: Account
-
- private var aor = ""
-
- private var backInvokedCallback: OnBackInvokedCallback? = null
- private lateinit var onBackPressedCallback: OnBackPressedCallback
-
- @RequiresApi(33)
- private fun registerBackInvokedCallback() {
- backInvokedCallback = OnBackInvokedCallback { goBack() }
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- backInvokedCallback!!
- )
- }
-
- public override fun onCreate(savedInstanceState: Bundle?) {
-
- super.onCreate(savedInstanceState)
-
- enableEdgeToEdge()
-
- if (Build.VERSION.SDK_INT >= 33)
- registerBackInvokedCallback()
- else {
- onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- goBack()
- }
- }
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
- val title = getString(R.string.call_history)
-
- aor = intent.getStringExtra("aor")!!
- Utils.addActivity("calls,$aor")
-
- val ua = UserAgent.ofAor(aor)!!
- account = ua.account
- aorGenerateHistory(aor)
-
- setContent {
- AppTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = LocalCustomColors.current.background
- ) {
- CallsScreen(this, title) { goBack() }
- }
- }
- }
-
- account.missedCalls = false
- }
-
- @Composable
- fun CallsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
- Scaffold(
- modifier = Modifier
- .fillMaxHeight()
- .imePadding()
- .safeDrawingPadding(),
- containerColor = LocalCustomColors.current.background,
- topBar = { TopAppBar(title, navigateBack) },
- content = { contentPadding ->
- CallsContent(ctx, contentPadding)
- }
- )
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- fun TopAppBar(title: String, navigateBack: () -> Unit) {
-
- var expanded by remember { mutableStateOf(false) }
-
- val delete = stringResource(R.string.delete)
- val disable = stringResource(R.string.disable_history)
- val enable = stringResource(R.string.enable_history)
-
- val showDialog = remember { mutableStateOf(false) }
- val positiveAction = remember { mutableStateOf({}) }
-
- AlertDialog(
- showDialog = showDialog,
- title = stringResource(R.string.confirmation),
- message = String.format(stringResource(R.string.delete_history_alert), aor.substringAfter(":")),
- positiveButtonText = stringResource(R.string.delete),
- negativeButtonText = stringResource(R.string.cancel),
- onPositiveClicked = positiveAction.value,
- )
-
- TopAppBar(
- title = {
- Text(
- text = title,
- color = LocalCustomColors.current.light,
- fontWeight = FontWeight.Bold
- )
- },
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = LocalCustomColors.current.primary
- ),
- navigationIcon = {
- IconButton(onClick = navigateBack) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = LocalCustomColors.current.light
- )
- }
- },
- actions = {
- IconButton(
- onClick = { expanded = !expanded }
- ) {
- Icon(
- imageVector = Icons.Filled.Menu,
- contentDescription = "Menu",
- tint = LocalCustomColors.current.light
- )
- }
- CustomElements.DropdownMenu(expanded,
- { expanded = false },
- listOf(delete, if (account.callHistory) disable else enable),
- onItemClick = { selectedItem ->
- expanded = false
- when (selectedItem) {
- delete -> {
- positiveAction.value = {
- CallHistoryNew.clear(aor)
- uaHistory.value = emptyList()
- }
- showDialog.value = true
- }
- disable, enable -> {
- account.callHistory = !account.callHistory
- AccountsActivity.saveAccounts()
- }
- }
- }
- )
- }
- )
- }
-
- @Composable
- fun CallsContent(ctx: Context, contentPadding: PaddingValues) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(contentPadding)
- .padding(bottom = 16.dp),
- verticalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- Account(account)
- Calls(ctx, account)
- }
- }
-
- @Composable
- fun Account(account: Account) {
- val headerText = stringResource(R.string.account) + " " +
- if (account.nickName.value != "")
- account.nickName.value
- else
- aor.split(":")[1]
- Text(
- text = headerText,
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 8.dp),
- fontSize = 18.sp,
- fontWeight = FontWeight.SemiBold,
- color = LocalCustomColors.current.itemText,
- textAlign = TextAlign.Center
- )
- }
-
- @OptIn(ExperimentalFoundationApi::class)
- @Composable
- fun Calls(ctx: Context, account: Account) {
-
- val showDialog = remember { mutableStateOf(false) }
- val message = remember { mutableStateOf("") }
- val positiveButtonText = remember { mutableStateOf("") }
- val positiveAction = remember { mutableStateOf({}) }
- val neutralButtonText = remember { mutableStateOf("") }
- val neutralAction = remember { mutableStateOf({}) }
-
- AlertDialog(
- showDialog = showDialog,
- title = stringResource(R.string.confirmation),
- message = message.value,
- positiveButtonText = positiveButtonText.value,
- onPositiveClicked = positiveAction.value,
- neutralButtonText = neutralButtonText.value,
- onNeutralClicked = neutralAction.value,
- negativeButtonText = stringResource(R.string.cancel)
- )
-
- val lazyListState = rememberLazyListState()
- LazyColumn(
- modifier = Modifier
- .imePadding()
- .fillMaxWidth()
- .padding(start = 16.dp, end = 4.dp)
- .verticalScrollbar(
- state = lazyListState,
- width = 4.dp,
- color = LocalCustomColors.current.gray
- )
- .background(LocalCustomColors.current.background),
- state = lazyListState,
- verticalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- items(items = uaHistory.value) { callRow ->
- var recordings = false
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.fillMaxWidth()
- ) {
- Box(modifier = Modifier.weight(1f)) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.combinedClickable(
- onClick = {
- val peerUri = callRow.peerUri
- val peerName = Utils.friendlyUri(ctx, peerUri, account)
- message.value = String.format(getString(R.string.contact_action_question), peerName)
- positiveButtonText.value = getString(R.string.call)
- positiveAction.value = {
- BaresipService.activities.remove("calls,$aor")
- MainActivity.activityAor = aor
- returnResult()
- val i = Intent(this@CallsActivity, MainActivity::class.java)
- i.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
- i.putExtra("action", "call")
- i.putExtra("uap", UserAgent.ofAor(aor)!!.uap)
- i.putExtra("peer", peerUri)
- startActivity(i)
- }
- neutralButtonText.value = getString(R.string.send_message)
- neutralAction.value = {
- BaresipService.activities.remove("calls,$aor")
- MainActivity.activityAor = aor
- returnResult()
- val i = Intent(this@CallsActivity, MainActivity::class.java)
- i.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
- i.putExtra("action", "message")
- i.putExtra("uap", UserAgent.ofAor(aor)!!.uap)
- i.putExtra("peer", peerUri)
- startActivity(i)
- }
- showDialog.value = true
- },
- onLongClick = {
- val peerUri = callRow.peerUri
- val peerName = Utils.friendlyUri(ctx, peerUri, account)
- val callText: String = if (callRow.details.size > 1)
- getString(R.string.calls_calls)
- else
- getString(R.string.calls_call)
- val contactExists = Contact.nameExists(peerName, BaresipService.contacts, false)
- if (contactExists) {
- message.value = String.format(
- getString(R.string.calls_delete_question),
- peerName, callText
- )
- positiveButtonText.value = getString(R.string.delete)
- positiveAction.value = {
- removeFromHistory(callRow)
- }
- neutralButtonText.value = ""
- }
- else {
- message.value = String.format(
- getString(R.string.calls_add_delete_question),
- peerName, callText
- )
- positiveButtonText.value = getString(R.string.add_contact)
- positiveAction.value = {
- val i = Intent(ctx, BaresipContactActivity::class.java)
- val b = Bundle()
- b.putBoolean("new", true)
- b.putString("uri", callRow.peerUri)
- i.putExtras(b)
- ctx.startActivity(i)
- }
- neutralButtonText.value = getString(R.string.delete)
- neutralAction.value = {
- removeFromHistory(callRow)
- }
- }
- showDialog.value = true
- }
- )
- ) {
- val uri = callRow.peerUri
- when (val contact = Contact.findContact(uri)) {
- is Contact.BaresipContact -> {
- val avatarImage = contact.avatarImage
- if (avatarImage != null)
- ImageAvatar(avatarImage)
- else
- TextAvatar(contact.name, contact.color)
- }
- is Contact.AndroidContact -> {
- val thumbNailUri = contact.thumbnailUri
- if (thumbNailUri != null)
- AsyncImage(
- model = thumbNailUri,
- contentDescription = "Avatar",
- contentScale = ContentScale.Crop,
- modifier = Modifier
- .size(36.dp)
- .clip(CircleShape),
- )
- else
- TextAvatar(contact.name, contact.color)
- }
- null -> {
- val avatarImage = BitmapFactory
- .decodeResource(ctx.resources, R.drawable.person_image)
- ImageAvatar(avatarImage)
- }
- }
- Spacer(modifier = Modifier.width(4.dp))
- var count = 1
- for (d in callRow.details) {
- if (d.recording[0] != "")
- recordings = true
- if (count > 3)
- continue
- Image(painterResource(d.direction), "Direction")
- count++
- }
- if (count > 3)
- Text("...", color = LocalCustomColors.current.itemText)
- Text(text = Utils.friendlyUri(ctx, callRow.peerUri, account),
- modifier = Modifier.padding(start = 8.dp),
- fontSize = 18.sp,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- color = LocalCustomColors.current.itemText
- )
- }
- }
- Box(modifier = Modifier.width(56.dp)) {
- Text(
- text = Utils.relativeTime(ctx, callRow.stopTime),
- fontSize = 12.sp,
- minLines = 2, maxLines = 2,
- lineHeight = 16.sp,
- textAlign = TextAlign.End,
- color = if (recordings)
- LocalCustomColors.current.accent
- else
- LocalCustomColors.current.itemText,
- modifier = Modifier
- .padding(end = 16.dp)
- .width(64.dp)
- .clickable(onClick = {
- val i = Intent(ctx, CallDetailsActivity::class.java)
- val b = Bundle()
- b.putString("aor", account.aor)
- b.putString("peer", callRow.peerUri)
- b.putInt("position", uaHistory.value.indexOf(callRow))
- i.putExtras(b)
- ctx.startActivity(i)
- })
- )
- }
- }
- }
- }
- }
-
- private fun goBack() {
- BaresipService.activities.remove("calls,$aor")
- returnResult()
- }
-
- private fun returnResult() {
- setResult(RESULT_CANCELED, Intent())
- finish()
- }
-
- override fun onPause() {
- MainActivity.activityAor = aor
- super.onPause()
- }
-
- override fun onDestroy() {
- if (Build.VERSION.SDK_INT >= 33) {
- if (backInvokedCallback != null)
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
- }
- else
- onBackPressedCallback.remove()
- super.onDestroy()
- }
-
- private fun aorGenerateHistory(aor: String) {
- uaHistory.value = emptyList()
- for (i in BaresipService.callHistory.indices.reversed()) {
- val h = BaresipService.callHistory[i]
- if (h.aor == aor) {
- val direction: Int = if (h.direction == "in") {
- if (h.startTime != null) {
- R.drawable.call_down_green
- } else {
- if (h.rejected)
- R.drawable.call_down_red
- else
- R.drawable.call_missed_in
- }
- } else {
- if (h.startTime != null) {
- R.drawable.call_up_green
- } else {
- if (h.rejected)
- R.drawable.call_up_red
- else
- R.drawable.call_missed_out
- }
- }
- if (uaHistory.value.isNotEmpty() && (uaHistory.value.last().peerUri == h.peerUri))
- uaHistory.value.last().details.add(CallRow.Details(
- direction, h.startTime,
- h.stopTime, h.recording
- ))
- else
- addToUaHistory(
- CallRow(h.aor, h.peerUri, direction, h.startTime, h.stopTime, h.recording)
- )
- }
- }
- }
-
- private fun removeFromHistory(callRow: CallRow) {
- for (details in callRow.details) {
- if (details.recording[0] != "")
- CallHistoryNew.deleteRecording(details.recording)
- BaresipService.callHistory.removeAll {
- it.startTime == details.startTime && it.stopTime == details.stopTime
- }
- }
- CallHistoryNew.deleteRecording(callRow.recording)
- deleteFromUaHistory(callRow)
- CallHistoryNew.save()
- }
-
- private fun addToUaHistory(callRow: CallRow) {
- val updatedList = uaHistory.value.toMutableList()
- updatedList.add(callRow)
- uaHistory.value = updatedList
- }
-
- private fun deleteFromUaHistory(callRow: CallRow) {
- val updatedList = uaHistory.value.toMutableList()
- updatedList.remove(callRow)
- uaHistory.value = updatedList
- }
-
- @SuppressLint("MutableCollectionMutableState")
- companion object {
- val uaHistory = mutableStateOf(emptyList())
- }
-
-}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt
new file mode 100644
index 00000000..1cf33423
--- /dev/null
+++ b/app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt
@@ -0,0 +1,483 @@
+package com.tutpro.baresip
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.BitmapFactory
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import coil.compose.AsyncImage
+import com.tutpro.baresip.CustomElements.AlertDialog
+import com.tutpro.baresip.CustomElements.verticalScrollbar
+
+fun NavGraphBuilder.callsScreenRoute(navController: NavController, viewModel: ViewModel) {
+ composable(
+ route = "calls/{aor}",
+ arguments = listOf(navArgument("aor") { type = NavType.StringType })
+ ) { backStackEntry ->
+ val aor = backStackEntry.arguments?.getString("aor")!!
+ CallsScreen(navController, viewModel, aor)
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun CallsScreen(navController: NavController, viewModel: ViewModel, aor: String) {
+
+ val account = Account.ofAor(aor)!!
+
+ val callHistory: MutableState> = remember { mutableStateOf(emptyList()) }
+
+ var isHistoryLoaded by remember { mutableStateOf(false) }
+ LaunchedEffect(aor) {
+ callHistory.value = loadCallHistory(aor)
+ isHistoryLoaded = true
+ }
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize().imePadding(),
+ containerColor = LocalCustomColors.current.background,
+ topBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
+ ) {
+ TopAppBar(navController, account, callHistory)
+ }
+ },
+ content = { contentPadding ->
+ if (isHistoryLoaded)
+ CallsContent(
+ LocalContext.current,
+ navController,
+ viewModel,
+ contentPadding,
+ account,
+ callHistory
+ )
+ },
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TopAppBar(navController: NavController, account: Account, callHistory: MutableState>) {
+
+ var expanded by remember { mutableStateOf(false) }
+
+ val delete = stringResource(R.string.delete)
+ val disable = stringResource(R.string.disable_history)
+ val enable = stringResource(R.string.enable_history)
+
+ val showDialog = remember { mutableStateOf(false) }
+ val positiveAction = remember { mutableStateOf({}) }
+
+ AlertDialog(
+ showDialog = showDialog,
+ title = stringResource(R.string.confirmation),
+ message = String.format(stringResource(R.string.delete_history_alert), account.text()),
+ positiveButtonText = stringResource(R.string.delete),
+ negativeButtonText = stringResource(R.string.cancel),
+ onPositiveClicked = positiveAction.value,
+ )
+
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(R.string.call_history),
+ color = LocalCustomColors.current.light,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ colors = TopAppBarDefaults.mediumTopAppBarColors(
+ containerColor = LocalCustomColors.current.primary
+ ),
+ windowInsets = WindowInsets(0, 0, 0, 0),
+ navigationIcon = {
+ IconButton(
+ onClick = {
+ navController.popBackStack()
+ account.missedCalls = false
+ }
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back",
+ tint = LocalCustomColors.current.light
+ )
+ }
+ },
+ actions = {
+ IconButton(
+ onClick = { expanded = !expanded }
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Menu,
+ contentDescription = "Menu",
+ tint = LocalCustomColors.current.light
+ )
+ }
+ CustomElements.DropdownMenu(
+ expanded,
+ { expanded = false },
+ listOf(delete, if (account.callHistory) disable else enable),
+ onItemClick = { selectedItem ->
+ expanded = false
+ when (selectedItem) {
+ delete -> {
+ positiveAction.value = {
+ CallHistoryNew.clear(account.aor)
+ callHistory.value = emptyList()
+ }
+ showDialog.value = true
+ }
+
+ disable, enable -> {
+ account.callHistory = !account.callHistory
+ Account.saveAccounts()
+ }
+ }
+ }
+ )
+ }
+ )
+}
+
+@Composable
+private fun CallsContent(
+ ctx: Context,
+ navController: NavController,
+ viewModel: ViewModel,
+ contentPadding: PaddingValues,
+ account: Account,
+ callHistory: MutableState>
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(contentPadding)
+ .padding(bottom = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Account(account)
+ Calls(ctx, navController, viewModel, account, callHistory)
+ }
+}
+
+@Composable
+private fun Account(account: Account) {
+ val headerText = stringResource(R.string.account) + " " +
+ if (account.nickName.value != "")
+ account.nickName.value
+ else
+ account.aor.split(":")[1]
+ Text(
+ text = headerText,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp),
+ fontSize = 18.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = LocalCustomColors.current.itemText,
+ textAlign = TextAlign.Center
+ )
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun Calls(
+ ctx: Context,
+ navController: NavController,
+ viewModel: ViewModel,
+ account: Account,
+ callHistory: MutableState>
+) {
+
+ val showDialog = remember { mutableStateOf(false) }
+ val message = remember { mutableStateOf("") }
+ val positiveButtonText = remember { mutableStateOf("") }
+ val positiveAction = remember { mutableStateOf({}) }
+ val neutralButtonText = remember { mutableStateOf("") }
+ val neutralAction = remember { mutableStateOf({}) }
+
+ AlertDialog(
+ showDialog = showDialog,
+ title = stringResource(R.string.confirmation),
+ message = message.value,
+ positiveButtonText = positiveButtonText.value,
+ onPositiveClicked = positiveAction.value,
+ neutralButtonText = neutralButtonText.value,
+ onNeutralClicked = neutralAction.value,
+ negativeButtonText = stringResource(R.string.cancel)
+ )
+
+ val lazyListState = rememberLazyListState()
+ LazyColumn(
+ modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 4.dp)
+ .verticalScrollbar(
+ state = lazyListState,
+ width = 4.dp,
+ color = LocalCustomColors.current.gray
+ )
+ .background(LocalCustomColors.current.background),
+ state = lazyListState,
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ items(items = callHistory.value, key = { callRow -> callRow.stopTime }) { callRow ->
+ var recordings = false
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Box(modifier = Modifier.weight(1f)) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.combinedClickable(
+ onClick = {
+ val aor = account.aor
+ val ua = UserAgent.ofAor(aor)
+ val peerUri = callRow.peerUri
+ val intent = Intent(ctx, MainActivity::class.java)
+ if (ua != null) {
+ intent.putExtra("uap", ua.uap)
+ intent.putExtra("peer", peerUri)
+ }
+ else
+ Log.w(TAG, "onClickListener did not find UA for $aor")
+ val peerName = Utils.friendlyUri(ctx, peerUri, account)
+ message.value = String.format(ctx.getString(R.string.contact_action_question), peerName)
+ positiveButtonText.value = ctx.getString(R.string.call)
+ positiveAction.value = {
+ if (ua != null) {
+ handleIntent(ctx, viewModel, intent, "call")
+ navController.popBackStack()
+ }
+ }
+ neutralButtonText.value = ctx.getString(R.string.send_message)
+ neutralAction.value = {
+ if (ua != null) {
+ handleIntent(ctx, viewModel, intent, "message")
+ navController.popBackStack()
+ }
+ }
+ showDialog.value = true
+ },
+ onLongClick = {
+ val peerUri = callRow.peerUri
+ val peerName = Utils.friendlyUri(ctx, peerUri, account)
+ val callText: String = if (callRow.details.size > 1)
+ ctx.getString(R.string.calls_calls)
+ else
+ ctx.getString(R.string.calls_call)
+ val contactExists = Contact.nameExists(peerName, BaresipService.contacts, false)
+ if (contactExists) {
+ message.value = String.format(
+ ctx.getString(R.string.calls_delete_question),
+ peerName, callText
+ )
+ positiveButtonText.value = ctx.getString(R.string.delete)
+ positiveAction.value = {
+ removeFromHistory(callHistory, callRow)
+ }
+ neutralButtonText.value = ""
+ }
+ else {
+ message.value = String.format(
+ ctx.getString(R.string.calls_add_delete_question),
+ peerName, callText
+ )
+ positiveButtonText.value = ctx.getString(R.string.add_contact)
+ positiveAction.value = {
+ navController.navigate("baresip_contact/${callRow.peerUri}/new")
+ }
+ neutralButtonText.value = ctx.getString(R.string.delete)
+ neutralAction.value = {
+ removeFromHistory(callHistory, callRow)
+ }
+ }
+ showDialog.value = true
+ }
+ )
+ ) {
+ val uri = callRow.peerUri
+ when (val contact = Contact.findContact(uri)) {
+ is Contact.BaresipContact -> {
+ val avatarImage = contact.avatarImage
+ if (avatarImage != null)
+ CustomElements.ImageAvatar(avatarImage)
+ else
+ CustomElements.TextAvatar(contact.name, contact.color)
+ }
+ is Contact.AndroidContact -> {
+ val thumbNailUri = contact.thumbnailUri
+ if (thumbNailUri != null)
+ AsyncImage(
+ model = thumbNailUri,
+ contentDescription = "Avatar",
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .size(36.dp)
+ .clip(CircleShape),
+ )
+ else
+ CustomElements.TextAvatar(contact.name, contact.color)
+ }
+ null -> {
+ val avatarImage = BitmapFactory
+ .decodeResource(ctx.resources, R.drawable.person_image)
+ CustomElements.ImageAvatar(avatarImage)
+ }
+ }
+ Spacer(modifier = Modifier.width(4.dp))
+ var count = 1
+ for (d in callRow.details) {
+ if (d.recording[0] != "")
+ recordings = true
+ if (count > 3)
+ continue
+ Image(painterResource(d.direction), "Direction")
+ count++
+ }
+ if (count > 3)
+ Text("...", color = LocalCustomColors.current.itemText)
+ Text(text = Utils.friendlyUri(ctx, callRow.peerUri, account),
+ modifier = Modifier.padding(start = 8.dp),
+ fontSize = 18.sp,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = LocalCustomColors.current.itemText
+ )
+ }
+ }
+ Box(modifier = Modifier.width(56.dp)) {
+ Text(
+ text = Utils.relativeTime(ctx, callRow.stopTime),
+ fontSize = 12.sp,
+ minLines = 2, maxLines = 2,
+ lineHeight = 16.sp,
+ textAlign = TextAlign.End,
+ color = if (recordings)
+ LocalCustomColors.current.accent
+ else
+ LocalCustomColors.current.itemText,
+ modifier = Modifier
+ .padding(end = 16.dp)
+ .width(64.dp)
+ .clickable(onClick = {
+ viewModel.selectCallRow(callRow)
+ navController.navigate("call_details")
+ })
+ )
+ }
+ }
+ }
+ }
+}
+
+private fun loadCallHistory(aor: String): MutableList {
+ val res = mutableListOf()
+ for (i in BaresipService.callHistory.indices.reversed()) {
+ val h = BaresipService.callHistory[i]
+ if (h.aor == aor) {
+ val direction: Int = if (h.direction == "in") {
+ if (h.startTime != null) {
+ R.drawable.call_down_green
+ } else {
+ if (h.rejected)
+ R.drawable.call_down_red
+ else
+ R.drawable.call_missed_in
+ }
+ } else {
+ if (h.startTime != null) {
+ R.drawable.call_up_green
+ } else {
+ if (h.rejected)
+ R.drawable.call_up_red
+ else
+ R.drawable.call_missed_out
+ }
+ }
+ if (res.isNotEmpty() && res.last().peerUri == h.peerUri)
+ res.last().details.add(CallRow.Details(
+ direction, h.startTime,
+ h.stopTime, h.recording
+ ))
+ else
+ res.add(CallRow(h.aor, h.peerUri, direction, h.startTime, h.stopTime, h.recording))
+ }
+ }
+ return res
+}
+
+private fun removeFromHistory(callHistory: MutableState>, callRow: CallRow) {
+ for (details in callRow.details) {
+ if (details.recording[0] != "")
+ CallHistoryNew.deleteRecording(details.recording)
+ BaresipService.callHistory.removeAll {
+ it.startTime == details.startTime && it.stopTime == details.stopTime
+ }
+ }
+ CallHistoryNew.deleteRecording(callRow.recording)
+ val updatedList = callHistory.value.filterNot { it == callRow }
+ callHistory.value = updatedList
+ CallHistoryNew.save()
+}
+
diff --git a/app/src/main/kotlin/com/tutpro/baresip/ChatActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/ChatActivity.kt
deleted file mode 100644
index 466447ce..00000000
--- a/app/src/main/kotlin/com/tutpro/baresip/ChatActivity.kt
+++ /dev/null
@@ -1,637 +0,0 @@
-package com.tutpro.baresip
-
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import android.os.Bundle
-import android.os.SystemClock
-import android.text.format.DateUtils.isToday
-import android.view.inputmethod.InputMethodManager
-import android.widget.Toast
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.navigationBarsPadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.foundation.text.selection.SelectionContainer
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.outlined.Clear
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalSoftwareKeyboardController
-import androidx.compose.ui.platform.SoftwareKeyboardController
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.res.vectorResource
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.KeyboardCapitalization
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.Observer
-import com.tutpro.baresip.CustomElements.AlertDialog
-import com.tutpro.baresip.CustomElements.EventListener
-import com.tutpro.baresip.CustomElements.LabelText
-import com.tutpro.baresip.CustomElements.verticalScrollbar
-import kotlinx.coroutines.launch
-import java.text.DateFormat
-import java.util.GregorianCalendar
-
-class ChatActivity : ComponentActivity() {
-
- private lateinit var imm: InputMethodManager
- private lateinit var aor: String
- private lateinit var account: Account
- private lateinit var peerUri: String
- private lateinit var ua: UserAgent
-
- private var _chatMessages = mutableStateOf>(emptyList())
- private var chatMessages : List by _chatMessages
- private var focus = false
- private var lastCall: Long = 0
- private var keyboardController: SoftwareKeyboardController? = null
- private val chatPeer = mutableStateOf("")
-
- private var backInvokedCallback: OnBackInvokedCallback? = null
- private lateinit var onBackPressedCallback: OnBackPressedCallback
-
- @RequiresApi(33)
- private fun registerBackInvokedCallback() {
- backInvokedCallback = OnBackInvokedCallback { goBack() }
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- backInvokedCallback!!
- )
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
-
- super.onCreate(savedInstanceState)
-
- enableEdgeToEdge()
-
- if (Build.VERSION.SDK_INT >= 33)
- registerBackInvokedCallback()
- else {
- onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- goBack()
- }
- }
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
- aor = intent.getStringExtra("aor")!!
- peerUri = intent.getStringExtra("peer")!!
- account = UserAgent.ofAor(aor)!!.account
- focus = intent.getBooleanExtra("focus", false)
-
- if (BaresipService.activities.first().startsWith("chat,$aor,$peerUri")) {
- returnResult(RESULT_CANCELED)
- return
- } else {
- Utils.addActivity("chat,$aor,$peerUri,$focus")
- }
-
- val userAgent = UserAgent.ofAor(aor)
- if (userAgent == null) {
- Log.w(TAG, "ChatActivity did not find ua of $aor")
- MainActivity.activityAor = aor
- returnResult(RESULT_CANCELED)
- return
- } else {
- ua = userAgent
- }
-
- chatPeer.value = Utils.friendlyUri(this, peerUri, userAgent.account, true)
-
- imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
-
- val messagesObserver = Observer {
- _chatMessages.value = listOf()
- _chatMessages.value = uaPeerMessages(aor, peerUri)
- }
- BaresipService.messageUpdate.observe(this, messagesObserver)
-
- ua.account.unreadMessages = false
-
- setContent {
- AppTheme {
- keyboardController = LocalSoftwareKeyboardController.current
- Surface(
- modifier = Modifier.fillMaxSize(),
- ) {
- ChatScreen(this) { goBack() }
- }
- }
- }
- }
-
- @Composable
- fun ChatScreen(ctx: Context, navigateBack: () -> Unit) {
- Scaffold(
- modifier = Modifier
- .fillMaxHeight()
- .imePadding()
- .safeDrawingPadding(),
- containerColor = LocalCustomColors.current.background,
- topBar = { TopAppBar(ctx, navigateBack) },
- bottomBar = { NewMessage(ctx, peerUri) },
- content = { contentPadding ->
- ChatContent(ctx, contentPadding)
- }
- )
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- fun TopAppBar(ctx: Context, navigateBack: () -> Unit) {
- TopAppBar(
- title = {
- Text(
- text = String.format(getString(R.string.chat_with), chatPeer.value),
- color = LocalCustomColors.current.light,
- fontSize = 22.sp,
- fontWeight = FontWeight.Bold
- )
- },
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = LocalCustomColors.current.primary
- ),
- navigationIcon = {
- IconButton(onClick = navigateBack) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = LocalCustomColors.current.light
- )
- }
- },
- actions = {
- IconButton(
- onClick = {
- if (SystemClock.elapsedRealtime() - lastCall > 1000) {
- lastCall = SystemClock.elapsedRealtime()
- val intent = Intent(ctx, MainActivity::class.java)
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
- Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
- intent.putExtra("action", "call")
- intent.putExtra("uap", ua.uap)
- intent.putExtra("peer", peerUri)
- startActivity(intent)
- }
- }
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(R.drawable.call_small),
- contentDescription = "Call",
- tint = LocalCustomColors.current.light
- )
- }
- }
- )
- }
-
- @Composable
- fun ChatContent(ctx: Context, contentPadding: PaddingValues) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(contentPadding),
- verticalArrangement = Arrangement.Bottom
- ) {
- Account(account)
- Spacer(modifier = Modifier.weight(1f))
- Messages(ctx)
- }
- }
-
- @Composable
- fun Account(account: Account) {
- val headerText = getString(R.string.account) + " " +
- if (account.nickName.value != "")
- account.nickName.value
- else
- aor.split(":")[1]
- Text(
- text = headerText,
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 8.dp, bottom = 8.dp),
- fontSize = 18.sp,
- fontWeight = FontWeight.SemiBold,
- textAlign = TextAlign.Center
- )
- }
-
- @Composable
- fun Messages(ctx: Context) {
-
- val showDialog = remember { mutableStateOf(false) }
- val dialogMessage = remember { mutableStateOf("") }
- val positiveButtonText = remember { mutableStateOf("") }
- val positiveAction = remember { mutableStateOf({}) }
- val neutralButtonText = remember { mutableStateOf("") }
- val neutralAction = remember { mutableStateOf({}) }
-
- AlertDialog(
- showDialog = showDialog,
- title = stringResource(R.string.confirmation),
- message = dialogMessage.value,
- positiveButtonText = positiveButtonText.value,
- onPositiveClicked = positiveAction.value,
- neutralButtonText = neutralButtonText.value,
- onNeutralClicked = neutralAction.value,
- negativeButtonText = stringResource(R.string.cancel)
- )
-
- val lazyListState = rememberLazyListState()
- val coroutineScope = rememberCoroutineScope()
-
- LaunchedEffect(Unit) {
- _chatMessages.value = uaPeerMessages(aor, peerUri)
- }
-
- LaunchedEffect(chatMessages) {
- // Scroll to the bottom when new messages are added
- if (chatMessages.isNotEmpty()) {
- coroutineScope.launch {
- lazyListState.scrollToItem(0)
- }
- }
- }
-
- LazyColumn(
- modifier = Modifier
- .imePadding()
- .fillMaxWidth()
- .padding(start = 16.dp, end = 2.dp)
- .verticalScrollbar(
- state = lazyListState,
- width = 4.dp,
- color = LocalCustomColors.current.gray
- ),
- reverseLayout = true,
- state = lazyListState,
- verticalArrangement = Arrangement.spacedBy(16.dp),
- ) {
- items(items = chatMessages, key = { message -> message.timeStamp }) { message ->
- val down = message.direction == MESSAGE_DOWN
- val peer: String = if (down) {
- if (chatPeer.value.startsWith("sip:") &&
- (Utils.uriHostPart(message.peerUri) == Utils.uriHostPart(message.aor)))
- Utils.uriUserPart(message.peerUri)
- else
- chatPeer.value
- }
- else
- stringResource(R.string.you)
- var info: String
- val cal = GregorianCalendar()
- cal.timeInMillis = message.timeStamp
- val fmt: DateFormat = if (isToday(message.timeStamp))
- DateFormat.getTimeInstance(DateFormat.SHORT)
- else
- DateFormat.getDateInstance(DateFormat.SHORT)
- info = fmt.format(cal.time)
- if (info.length < 6) info = "${stringResource(R.string.today)} $info"
- if (message.direction == MESSAGE_UP_FAIL) {
- info = if (message.responseCode != 0)
- "$info - ${stringResource(R.string.message_failed)}: " + "${message.responseCode} ${message.responseReason}"
- else
- "$info - ${stringResource(R.string.sending_failed)}"
- }
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(end = 12.dp)
- ) {
- Button(
- onClick = {
- if (Contact.findContact(peerUri) == null) {
- dialogMessage.value = String.format(getString(R.string.long_message_question),
- chatPeer.value
- )
- positiveButtonText.value = getString(R.string.add_contact)
- positiveAction.value = {
- val i = Intent(ctx, BaresipContactActivity::class.java)
- val b = Bundle()
- b.putBoolean("new", true)
- b.putString("uri", peerUri)
- i.putExtras(b)
- ctx.startActivity(i)
- }
- neutralButtonText.value = getString(R.string.delete)
- neutralAction.value = {
- message.delete()
- _chatMessages.value = uaPeerMessages(aor, peerUri)
- }
- }
- else {
- dialogMessage.value = getString(R.string.short_message_question)
- positiveButtonText.value = getString(R.string.delete)
- positiveAction.value = {
- message.delete()
- _chatMessages.value = uaPeerMessages(aor, peerUri)
- }
- neutralButtonText.value = ""
- }
- showDialog.value = true
- },
- shape = if (message.direction == MESSAGE_DOWN)
- RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp)
- else
- RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp),
- colors = ButtonDefaults.buttonColors(containerColor =
- if (message.direction == MESSAGE_DOWN) {
- if (BaresipService.darkTheme.value)
- LocalCustomColors.current.secondaryDark
- else
- LocalCustomColors.current.secondaryLight
- }
- else {
- if (BaresipService.darkTheme.value)
- LocalCustomColors.current.primaryDark
- else
- LocalCustomColors.current.primaryLight
- }),
- modifier = Modifier
- .fillMaxWidth()
- .wrapContentHeight()
- .padding(
- start = if (message.direction == MESSAGE_DOWN) 0.dp else 24.dp,
- end = if (message.direction == MESSAGE_DOWN) 24.dp else 0.dp
- )
- ) {
- Column {
- Row {
- val textColor =
- if (message.direction == MESSAGE_DOWN) {
- if (BaresipService.darkTheme.value)
- LocalCustomColors.current.secondaryLight
- else
- LocalCustomColors.current.secondaryDark
- }
- else {
- if (BaresipService.darkTheme.value)
- LocalCustomColors.current.primaryLight
- else
- LocalCustomColors.current.primaryDark
- }
- Text(text = peer, fontSize = 12.sp, color = textColor)
- Spacer(modifier = Modifier.weight(1f))
- Text(text = info, fontSize = 12.sp, color = textColor)
- }
- Row {
- SelectionContainer {
- Text(
- text = message.message,
- color = LocalCustomColors.current.itemText,
- fontWeight = if (message.direction == MESSAGE_DOWN && message.new)
- FontWeight.Bold else FontWeight.Normal
- )
- }
- }
- }
- }
- }
- }
- }
- }
-
- @OptIn(ExperimentalFoundationApi::class)
- @Composable
- fun NewMessage(ctx: Context, peerUri: String) {
-
- var newMessage = rememberSaveable(stateSaver = TextFieldValue.Saver) {
- mutableStateOf(TextFieldValue(""))
- }
- var textFieldLoaded by remember { mutableStateOf(false) }
- val focusRequester = remember { FocusRequester() }
-
- EventListener {
- when (it) {
- Lifecycle.Event.ON_RESUME -> {
- val chatText = BaresipService.chatTexts["$aor::$peerUri"]
- if (chatText != null) {
- Log.d(TAG, "Restoring newMessage '$chatText' for $aor::$peerUri")
- newMessage.value = TextFieldValue(
- text = chatText,
- selection = TextRange(chatText.length)
- )
- BaresipService.chatTexts.remove("$aor::$peerUri")
- }
- }
- Lifecycle.Event.ON_PAUSE -> {
- if (newMessage.value != TextFieldValue("")) {
- Log.d(TAG, "Saving newMessage '${newMessage.value.text}' for $aor::$peerUri")
- BaresipService.chatTexts["$aor::$peerUri"] = newMessage.value.text
- }
- }
- else -> {}
- }
- }
-
- val showDialog = remember { mutableStateOf(false) }
- val dialogMessage = remember { mutableStateOf("") }
-
- AlertDialog(
- showDialog = showDialog,
- title = stringResource(R.string.notice),
- message = dialogMessage.value,
- positiveButtonText = stringResource(R.string.ok),
- )
-
- Row(modifier = Modifier
- .fillMaxWidth()
- .navigationBarsPadding()
- .padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- val keyboardController = LocalSoftwareKeyboardController.current
- OutlinedTextField(
- value = newMessage.value,
- placeholder = { Text(stringResource(R.string.new_message)) },
- onValueChange = { newMessage.value = it },
- modifier = Modifier
- .weight(1f)
- .padding(end = 8.dp)
- .verticalScroll(rememberScrollState())
- .focusRequester(focusRequester)
- .onGloballyPositioned {
- if (!textFieldLoaded)
- textFieldLoaded = true
- },
- singleLine = false,
- trailingIcon = {
- if (newMessage.value.text.isNotEmpty()) {
- Icon(
- Icons.Outlined.Clear,
- contentDescription = "Clear",
- modifier = Modifier.clickable { newMessage.value = TextFieldValue("") }
- )
- } },
- label = { LabelText(stringResource(R.string.new_message)) },
- textStyle = TextStyle(fontSize = 18.sp),
- keyboardOptions = KeyboardOptions(
- capitalization = KeyboardCapitalization.Sentences,
- keyboardType = KeyboardType.Text,
- autoCorrectEnabled = true
- )
- )
- LaunchedEffect(Unit) {
- if (newMessage.value.text.isNotEmpty())
- focusRequester.requestFocus()
- }
- Image(
- painter = painterResource(id = R.drawable.send),
- contentDescription = "Send",
- contentScale = ContentScale.Crop,
- modifier = Modifier
- .size(36.dp)
- .clickable {
- val msgText = newMessage.value.text
- if (msgText.isNotEmpty()) {
- keyboardController?.hide()
- val time = System.currentTimeMillis()
- val msg =
- Message(aor, peerUri, msgText, time, MESSAGE_UP_WAIT, 0, "", true)
- msg.add()
- var msgUri = ""
- _chatMessages.value += msg
- if (Utils.isTelUri(peerUri))
- if (ua.account.telProvider == "") {
- dialogMessage.value = String.format(
- getString(R.string.no_telephony_provider),
- Utils.plainAor(aor)
- )
- showDialog.value = true
- } else {
- msgUri = Utils.telToSip(peerUri, ua.account)
- }
- else
- msgUri = peerUri
- if (msgUri != "")
- if (Api.message_send(ua.uap, msgUri, msgText, time.toString()) != 0
- ) {
- Toast.makeText(
- ctx, "${getString(R.string.message_failed)}!",
- Toast.LENGTH_SHORT
- ).show()
- msg.direction = MESSAGE_UP_FAIL
- msg.responseReason = getString(R.string.message_failed)
- } else {
- newMessage.value = TextFieldValue("")
- keyboardController?.hide()
- BaresipService.chatTexts.remove("$aor::$peerUri")
- }
- }
- }
- )
- }
- }
-
- override fun onPause() {
- super.onPause()
- MainActivity.activityAor = aor
- }
-
- override fun onResume() {
- super.onResume()
- _chatMessages.value = uaPeerMessages(aor, peerUri)
- chatPeer.value = Utils.friendlyUri(this, peerUri, ua.account, true)
- }
-
- override fun onDestroy() {
- if (Build.VERSION.SDK_INT >= 33) {
- if (backInvokedCallback != null)
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
- }
- else
- onBackPressedCallback.remove()
- super.onDestroy()
- }
-
- private fun goBack() {
- var save = false
- for (m in chatMessages) {
- if (m.new) {
- m.new = false
- save = true
- }
- }
- if (save) Message.save()
- keyboardController?.hide()
- BaresipService.activities.remove("chat,$aor,$peerUri,false")
- BaresipService.activities.remove("chat,$aor,$peerUri,true")
- returnResult(RESULT_OK)
- }
-
- private fun returnResult(code: Int) {
- setResult(code, Intent())
- finish()
- }
-
- private fun uaPeerMessages(aor: String, peerUri: String): List {
- val res = mutableListOf()
- for (m in BaresipService.messages.reversed())
- if ((m.aor == aor) && (m.peerUri == peerUri)) res.add(m)
- return res
- }
-
-}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/ChatScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/ChatScreen.kt
new file mode 100644
index 00000000..e0b365fd
--- /dev/null
+++ b/app/src/main/kotlin/com/tutpro/baresip/ChatScreen.kt
@@ -0,0 +1,580 @@
+package com.tutpro.baresip
+
+import android.content.Context
+import android.content.Intent
+import android.text.format.DateUtils.isToday
+import android.widget.Toast
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.outlined.Clear
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.Observer
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import com.tutpro.baresip.CustomElements.AlertDialog
+import com.tutpro.baresip.CustomElements.LabelText
+import com.tutpro.baresip.CustomElements.verticalScrollbar
+import kotlinx.coroutines.launch
+import java.lang.String.format
+import java.text.DateFormat
+import java.util.GregorianCalendar
+
+fun NavGraphBuilder.chatScreenRoute(navController: NavController, viewModel: ViewModel) {
+ composable(
+ route = "chat/{aor}/{peer}",
+ arguments = listOf(
+ navArgument("aor") { type = NavType.StringType },
+ navArgument("peer") { type = NavType.StringType }
+ )
+ ) { backStackEntry ->
+ val aor = backStackEntry.arguments?.getString("aor")!!
+ val peerUri = backStackEntry.arguments?.getString("peer")!!
+ ChatScreen(
+ ctx = LocalContext.current,
+ navController = navController,
+ viewModel = viewModel,
+ account = Account.ofAor(aor)!!,
+ peerUri = peerUri
+ )
+ }
+}
+
+@Composable
+private fun ChatScreen(
+ ctx: Context,
+ navController: NavController,
+ viewModel: ViewModel,
+ account: Account,
+ peerUri: String
+) {
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ val aor = account.aor
+
+ var chatMessages by remember(aor, peerUri) { mutableStateOf>(emptyList()) }
+
+ DisposableEffect(lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_RESUME) {
+ Log.d(TAG, "Resumed to ChatScreen for AOR: $aor peer $peerUri")
+ chatMessages = loadPeerMessages(aor, peerUri)
+ }
+ }
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
+ var areMessagesLoaded by remember(aor, peerUri) {
+ mutableStateOf(false)
+ }
+
+ val reloadMessages = {
+ Log.d(TAG, "Reloading messages for $aor peer $peerUri")
+ chatMessages = loadPeerMessages(aor, peerUri)
+ if (!areMessagesLoaded)
+ areMessagesLoaded = true
+ }
+
+ val addMessage = { newMessage: Message ->
+ chatMessages = chatMessages + newMessage
+ }
+
+ DisposableEffect(key1 = lifecycleOwner, key2 = account.aor, key3 = peerUri) {
+ val messagesObserver = Observer { timestamp ->
+ Log.d(TAG, "Message update received via LiveData for $peerUri, timestamp: $timestamp")
+ reloadMessages()
+ }
+ reloadMessages() // Initial load
+ Log.d(TAG, "Observing message updates for $peerUri")
+ BaresipService.messageUpdate.observe(lifecycleOwner, messagesObserver)
+ onDispose {
+ Log.d(TAG, "Removing message observer for $peerUri")
+ BaresipService.messageUpdate.removeObserver(messagesObserver)
+ }
+ }
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize().imePadding(),
+ containerColor = LocalCustomColors.current.background,
+ topBar = {
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
+ ) {
+ TopAppBar(ctx, navController, viewModel, account, peerUri)
+ }
+ },
+ bottomBar = { NewMessage(ctx, account, peerUri, addMessage) },
+ content = { contentPadding ->
+ if (areMessagesLoaded)
+ ChatContent(ctx, navController, contentPadding, account, peerUri, chatMessages, reloadMessages)
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TopAppBar(
+ ctx: Context,
+ navController: NavController,
+ viewModel: ViewModel,
+ account: Account,
+ peerUri: String
+) {
+ val aor = account.aor
+
+ TopAppBar(
+ title = {
+ Text(
+ text = format(ctx.getString(R.string.chat_with), Utils.friendlyUri(ctx, peerUri, account)),
+ color = LocalCustomColors.current.light,
+ fontSize = 22.sp,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ colors = TopAppBarDefaults.mediumTopAppBarColors(
+ containerColor = LocalCustomColors.current.primary
+ ),
+ navigationIcon = {
+ IconButton(onClick = {
+ Message.updateMessagesFromPearRead(aor, peerUri)
+ account.unreadMessages = Message.unreadMessages(aor)
+ navController.popBackStack()
+ }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back",
+ tint = LocalCustomColors.current.light
+ )
+ }
+ },
+ windowInsets = WindowInsets(0, 0, 0, 0),
+ actions = {
+ IconButton(
+ onClick = {
+ val ua = UserAgent.ofAor(account.aor)
+ if (ua != null) {
+ val intent = Intent(ctx, MainActivity::class.java)
+ intent.putExtra("uap", ua.uap)
+ intent.putExtra("peer", peerUri)
+ handleIntent(ctx, viewModel, intent, "call")
+ navController.navigate("main") {
+ popUpTo("main")
+ launchSingleTop = true
+ }
+ }
+ else
+ Log.w(TAG, "onClickListener did not find UA for $aor")
+ }
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(R.drawable.call_small),
+ contentDescription = "Call",
+ tint = LocalCustomColors.current.light
+ )
+ }
+ }
+ )
+}
+
+@Composable
+private fun ChatContent(
+ ctx: Context,
+ navController: NavController,
+ contentPadding: PaddingValues,
+ account: Account,
+ peerUri: String,
+ messages: List,
+ onMessageDeleted: () -> Unit
+) {
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(contentPadding),
+ verticalArrangement = Arrangement.Bottom
+ ) {
+ Account(ctx, account)
+ Spacer(modifier = Modifier.weight(1f))
+ Messages(ctx, navController, account, peerUri, messages, onMessageDeleted)
+ }
+}
+
+@Composable
+private fun Account(ctx: Context, account: Account) {
+ val headerText = ctx.getString(R.string.account) + " " +
+ if (account.nickName.value != "")
+ account.nickName.value
+ else
+ account.aor.substringAfter(":")
+ Text(
+ text = headerText,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, bottom = 8.dp),
+ fontSize = 18.sp,
+ fontWeight = FontWeight.SemiBold,
+ textAlign = TextAlign.Center
+ )
+}
+
+@Composable
+private fun Messages(
+ ctx: Context,
+ navController: NavController,
+ account: Account,
+ peerUri: String,
+ messages: List,
+ onMessageDeleted: () -> Unit
+) {
+ val peerName = Utils.friendlyUri(ctx, peerUri, account)
+
+ val showDialog = remember { mutableStateOf(false) }
+ val dialogMessage = remember { mutableStateOf("") }
+ val positiveButtonText = remember { mutableStateOf("") }
+ val positiveAction = remember { mutableStateOf({}) }
+ val neutralButtonText = remember { mutableStateOf("") }
+ val neutralAction = remember { mutableStateOf({}) }
+
+ AlertDialog(
+ showDialog = showDialog,
+ title = stringResource(R.string.confirmation),
+ message = dialogMessage.value,
+ positiveButtonText = positiveButtonText.value,
+ onPositiveClicked = positiveAction.value,
+ neutralButtonText = neutralButtonText.value,
+ onNeutralClicked = neutralAction.value,
+ negativeButtonText = stringResource(R.string.cancel)
+ )
+
+ val lazyListState = rememberLazyListState()
+ val coroutineScope = rememberCoroutineScope()
+
+ LaunchedEffect(messages) {
+ // Scroll to the bottom when new messages are added
+ if (messages.isNotEmpty()) {
+ coroutineScope.launch {
+ lazyListState.scrollToItem(0)
+ }
+ }
+ }
+
+ LazyColumn(
+ modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 2.dp)
+ .verticalScrollbar(
+ state = lazyListState,
+ width = 4.dp,
+ color = LocalCustomColors.current.gray
+ ),
+ reverseLayout = true,
+ state = lazyListState,
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ items(items = messages, key = { message -> message.timeStamp }) { message ->
+ val down = message.direction == MESSAGE_DOWN
+ val sender: String = if (down)
+ peerName
+ else if (BaresipService.uas.value.size == 1)
+ stringResource(R.string.you)
+ else
+ account.text()
+ var info: String
+ val cal = GregorianCalendar()
+ cal.timeInMillis = message.timeStamp
+ val fmt: DateFormat = if (isToday(message.timeStamp))
+ DateFormat.getTimeInstance(DateFormat.SHORT)
+ else
+ DateFormat.getDateInstance(DateFormat.SHORT)
+ info = fmt.format(cal.time)
+ if (info.length < 6) info = "${stringResource(R.string.today)} $info"
+ if (message.direction == MESSAGE_UP_FAIL) {
+ info = if (message.responseCode != 0)
+ "$info - ${stringResource(R.string.message_failed)}: " + "${message.responseCode} ${message.responseReason}"
+ else
+ "$info - ${stringResource(R.string.sending_failed)}"
+ }
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(end = 12.dp)
+ ) {
+ Button(
+ onClick = {
+ if (Contact.findContact(peerUri) == null) {
+ dialogMessage.value = String.format(
+ ctx.getString(R.string.long_message_question),
+ peerUri
+ )
+ positiveButtonText.value = ctx.getString(R.string.add_contact)
+ positiveAction.value = {
+ navController.navigate("baresip_contact/$peerUri/new")
+ }
+ neutralButtonText.value = ctx.getString(R.string.delete)
+ neutralAction.value = {
+ message.delete()
+ onMessageDeleted
+ }
+ } else {
+ dialogMessage.value = ctx.getString(R.string.short_message_question)
+ positiveButtonText.value = ctx.getString(R.string.delete)
+ positiveAction.value = {
+ message.delete()
+ onMessageDeleted()
+ }
+ neutralButtonText.value = ""
+ }
+ showDialog.value = true
+ },
+ shape = if (message.direction == MESSAGE_DOWN)
+ RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp)
+ else
+ RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor =
+ if (message.direction == MESSAGE_DOWN) {
+ if (BaresipService.darkTheme.value)
+ LocalCustomColors.current.secondaryDark
+ else
+ LocalCustomColors.current.secondaryLight
+ } else {
+ if (BaresipService.darkTheme.value)
+ LocalCustomColors.current.primaryDark
+ else
+ LocalCustomColors.current.primaryLight
+ }
+ ),
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(
+ start = if (message.direction == MESSAGE_DOWN) 0.dp else 24.dp,
+ end = if (message.direction == MESSAGE_DOWN) 24.dp else 0.dp
+ )
+ ) {
+ Column {
+ Row {
+ val textColor =
+ if (message.direction == MESSAGE_DOWN) {
+ if (BaresipService.darkTheme.value)
+ LocalCustomColors.current.secondaryLight
+ else
+ LocalCustomColors.current.secondaryDark
+ } else {
+ if (BaresipService.darkTheme.value)
+ LocalCustomColors.current.primaryLight
+ else
+ LocalCustomColors.current.primaryDark
+ }
+ Text(text = sender, fontSize = 12.sp, color = textColor)
+ Spacer(modifier = Modifier.weight(1f))
+ Text(text = info, fontSize = 12.sp, color = textColor)
+ }
+ Row {
+ SelectionContainer {
+ Text(
+ text = message.message,
+ color = LocalCustomColors.current.itemText,
+ fontWeight = if (message.direction == MESSAGE_DOWN && message.new)
+ FontWeight.Bold else FontWeight.Normal
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun NewMessage(
+ ctx: Context,
+ account: Account,
+ peerUri: String,
+ addMessage: (message: Message) -> Unit
+) {
+
+ val aor = account.aor
+ val ua = UserAgent.ofAor(aor)!!
+
+ var newMessage = rememberSaveable(stateSaver = TextFieldValue.Saver) {
+ mutableStateOf(TextFieldValue(""))
+ }
+ var textFieldLoaded by remember { mutableStateOf(false) }
+ val focusRequester = remember { FocusRequester() }
+
+ val showDialog = remember { mutableStateOf(false) }
+ val dialogMessage = remember { mutableStateOf("") }
+
+ AlertDialog(
+ showDialog = showDialog,
+ title = stringResource(R.string.notice),
+ message = dialogMessage.value,
+ positiveButtonText = stringResource(R.string.ok),
+ )
+
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .navigationBarsPadding()
+ .padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val keyboardController = LocalSoftwareKeyboardController.current
+ OutlinedTextField(
+ value = newMessage.value,
+ placeholder = { Text(stringResource(R.string.new_message)) },
+ onValueChange = { newMessage.value = it },
+ modifier = Modifier
+ .weight(1f)
+ .padding(end = 8.dp)
+ .verticalScroll(rememberScrollState())
+ .focusRequester(focusRequester)
+ .onGloballyPositioned {
+ if (!textFieldLoaded)
+ textFieldLoaded = true
+ },
+ singleLine = false,
+ trailingIcon = {
+ if (newMessage.value.text.isNotEmpty()) {
+ Icon(
+ Icons.Outlined.Clear,
+ contentDescription = "Clear",
+ modifier = Modifier.clickable { newMessage.value = TextFieldValue("") }
+ )
+ } },
+ label = { LabelText(stringResource(R.string.new_message)) },
+ textStyle = TextStyle(fontSize = 18.sp),
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.Sentences,
+ keyboardType = KeyboardType.Text,
+ autoCorrectEnabled = true
+ )
+ )
+ LaunchedEffect(Unit) {
+ if (newMessage.value.text.isNotEmpty())
+ focusRequester.requestFocus()
+ }
+ Image(
+ painter = painterResource(id = R.drawable.send),
+ contentDescription = "Send",
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .size(36.dp)
+ .clickable {
+ val msgText = newMessage.value.text
+ if (msgText.isNotEmpty()) {
+ keyboardController?.hide()
+ val time = System.currentTimeMillis()
+ val msg =
+ Message(aor, peerUri, msgText, time, MESSAGE_UP_WAIT, 0, "", true)
+ msg.add()
+ var msgUri = ""
+ addMessage(msg)
+ if (Utils.isTelUri(peerUri))
+ if (ua.account.telProvider == "") {
+ dialogMessage.value = String.format(
+ ctx.getString(R.string.no_telephony_provider),
+ Utils.plainAor(aor)
+ )
+ showDialog.value = true
+ } else {
+ msgUri = Utils.telToSip(peerUri, ua.account)
+ }
+ else
+ msgUri = peerUri
+ if (msgUri != "")
+ if (Api.message_send(ua.uap, msgUri, msgText, time.toString()) != 0
+ ) {
+ Toast.makeText(
+ ctx, "${ctx.getString(R.string.message_failed)}!",
+ Toast.LENGTH_SHORT
+ ).show()
+ msg.direction = MESSAGE_UP_FAIL
+ msg.responseReason = ctx.getString(R.string.message_failed)
+ } else {
+ newMessage.value = TextFieldValue("")
+ keyboardController?.hide()
+ }
+ }
+ }
+ )
+ }
+}
+
+private fun loadPeerMessages(aor: String, peerUri: String): List {
+ val res = mutableListOf()
+ for (m in BaresipService.messages.reversed())
+ if ((m.aor == aor) && (m.peerUri == peerUri)) res.add(m)
+ return res
+}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/ChatsActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/ChatsActivity.kt
deleted file mode 100644
index 1bf33129..00000000
--- a/app/src/main/kotlin/com/tutpro/baresip/ChatsActivity.kt
+++ /dev/null
@@ -1,728 +0,0 @@
-package com.tutpro.baresip
-
-import android.content.Context
-import android.content.Intent
-import android.graphics.BitmapFactory
-import android.os.Build
-import android.os.Bundle
-import android.text.format.DateUtils.isToday
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.annotation.RequiresApi
-import androidx.compose.animation.animateContentSize
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.navigationBarsPadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Menu
-import androidx.compose.material.icons.outlined.Clear
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SmallFloatingActionButton
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import coil.compose.AsyncImage
-import com.tutpro.baresip.BaresipService.Companion.contactNames
-import com.tutpro.baresip.CustomElements.AlertDialog
-import com.tutpro.baresip.CustomElements.ImageAvatar
-import com.tutpro.baresip.CustomElements.LabelText
-import com.tutpro.baresip.CustomElements.TextAvatar
-import com.tutpro.baresip.CustomElements.verticalScrollbar
-import com.tutpro.baresip.CustomElements.SelectableAlertDialog
-import java.text.DateFormat
-import java.util.GregorianCalendar
-
-class ChatsActivity: ComponentActivity() {
-
- internal lateinit var aor: String
- internal lateinit var account: Account
- private lateinit var chatRequest: ActivityResultLauncher
-
- private var _uaMessages = mutableStateOf>(emptyList())
- private var uaMessages: List by _uaMessages
-
- private val alertTitle = mutableStateOf("")
- private val alertMessage = mutableStateOf("")
- private val showAlert = mutableStateOf(false)
-
- private var backInvokedCallback: OnBackInvokedCallback? = null
- private lateinit var onBackPressedCallback: OnBackPressedCallback
-
- @RequiresApi(33)
- private fun registerBackInvokedCallback() {
- backInvokedCallback = OnBackInvokedCallback { goBack() }
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- backInvokedCallback!!
- )
- }
-
- public override fun onCreate(savedInstanceState: Bundle?) {
-
- super.onCreate(savedInstanceState)
-
- enableEdgeToEdge()
-
- if (Build.VERSION.SDK_INT >= 33)
- registerBackInvokedCallback()
- else {
- onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- goBack()
- }
- }
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
- aor = intent.extras!!.getString("aor")!!
- Utils.addActivity("chats,$aor")
- account = UserAgent.ofAor(aor)!!.account
-
- val title = getString(R.string.chats)
-
- _uaMessages.value = uaMessages(aor)
-
- chatRequest =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == RESULT_OK)
- _uaMessages.value = uaMessages(aor)
- }
-
- setContent {
- AppTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = LocalCustomColors.current.background
- ) {
- ChatsScreen(this, title) { goBack() }
- }
- }
- }
- }
-
- @Composable
- fun ChatsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
- Scaffold(
- modifier = Modifier
- .fillMaxHeight()
- .imePadding()
- .safeDrawingPadding(),
- containerColor = LocalCustomColors.current.background,
- topBar = { TopAppBar(title, navigateBack) },
- bottomBar = { NewChatPeer() },
- content = { contentPadding ->
- ChatsContent(ctx, contentPadding)
- }
- )
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- fun TopAppBar(title: String, navigateBack: () -> Unit) {
-
- var expanded by remember { mutableStateOf(false) }
-
- val delete = stringResource(R.string.delete)
-
- val showDialog = remember { mutableStateOf(false) }
- val positiveAction = remember { mutableStateOf({}) }
-
- AlertDialog(
- showDialog = showDialog,
- title = stringResource(R.string.confirmation),
- message = String.format(
- stringResource(R.string.delete_chats_alert),
- aor.substringAfter(":")
- ),
- positiveButtonText = stringResource(R.string.delete),
- onPositiveClicked = positiveAction.value,
- negativeButtonText = stringResource(R.string.cancel),
- )
-
- TopAppBar(
- title = {
- Text(
- text = title,
- color = LocalCustomColors.current.light,
- fontSize = 22.sp,
- fontWeight = FontWeight.Bold
- )
- },
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = LocalCustomColors.current.primary
- ),
- navigationIcon = {
- IconButton(onClick = navigateBack) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = LocalCustomColors.current.light
- )
- }
- },
- actions = {
- IconButton(
- onClick = { expanded = !expanded }
- ) {
- Icon(
- imageVector = Icons.Filled.Menu,
- contentDescription = "Menu",
- tint = LocalCustomColors.current.light
- )
- }
- CustomElements.DropdownMenu(
- expanded,
- { expanded = false },
- listOf(delete),
- onItemClick = { selectedItem ->
- expanded = false
- when (selectedItem) {
- delete -> {
- positiveAction.value = {
- Message.clearMessagesOfAor(aor)
- Message.save()
- _uaMessages.value = listOf()
- account.unreadMessages = false
- }
- showDialog.value = true
- }
- }
- }
- )
- }
- )
- }
-
- @Composable
- fun ChatsContent(ctx: Context, contentPadding: PaddingValues) {
-
- if (showAlert.value) {
- AlertDialog(
- showDialog = showAlert,
- title = alertTitle.value,
- message = alertMessage.value,
- positiveButtonText = stringResource(R.string.ok),
- )
- }
-
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(contentPadding),
- verticalArrangement = Arrangement.Top
- ) {
- Account(account)
- Chats(ctx, account)
- }
- }
-
- @Composable
- fun Account(account: Account) {
- val headerText = stringResource(R.string.account) + " " +
- if (account.nickName.value != "")
- account.nickName.value
- else
- aor.split(":")[1]
- Text(
- text = headerText,
- modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp),
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp,
- fontWeight = FontWeight.SemiBold,
- textAlign = TextAlign.Center
- )
- }
-
- @Composable
- fun Chats(ctx: Context, account: Account) {
-
- val showDialog = remember { mutableStateOf(false) }
- val dialogMessage = remember { mutableStateOf("") }
- val positiveButtonText = remember { mutableStateOf("") }
- val positiveAction = remember { mutableStateOf({}) }
- val neutralButtonText = remember { mutableStateOf("") }
- val neutralAction = remember { mutableStateOf({}) }
-
- if (showDialog.value)
- AlertDialog(
- showDialog = showDialog,
- title = stringResource(R.string.confirmation),
- message = dialogMessage.value,
- positiveButtonText = positiveButtonText.value,
- onPositiveClicked = positiveAction.value,
- neutralButtonText = neutralButtonText.value,
- onNeutralClicked = neutralAction.value,
- negativeButtonText = stringResource(R.string.cancel)
- )
-
- val lazyListState = rememberLazyListState()
-
- LazyColumn(
- modifier = Modifier
- .imePadding()
- .fillMaxWidth()
- .padding(start = 8.dp, end = 4.dp)
- .verticalScrollbar(
- state = lazyListState,
- width = 4.dp,
- color = LocalCustomColors.current.gray
- )
- .background(LocalCustomColors.current.background),
- reverseLayout = true,
- state = lazyListState,
- verticalArrangement = Arrangement.spacedBy(16.dp),
- ) {
- items(items = uaMessages, key = { message -> message.timeStamp }) { message ->
- Row(verticalAlignment = Alignment.CenterVertically) {
- when (val contact = Contact.findContact(message.peerUri)) {
- is Contact.BaresipContact -> {
- val avatarImage = contact.avatarImage
- if (avatarImage != null)
- ImageAvatar(avatarImage)
- else
- TextAvatar(contact.name, contact.color)
- }
-
- is Contact.AndroidContact -> {
- val thumbNailUri = contact.thumbnailUri
- if (thumbNailUri != null)
- AsyncImage(
- model = thumbNailUri,
- contentDescription = "Avatar",
- contentScale = ContentScale.Crop,
- modifier = Modifier
- .size(36.dp)
- .clip(CircleShape),
- )
- else
- TextAvatar(contact.name, contact.color)
- }
-
- null -> {
- val avatarImage = BitmapFactory
- .decodeResource(ctx.resources, R.drawable.person_image)
- ImageAvatar(avatarImage)
- }
- }
- Spacer(modifier = Modifier.width(6.dp))
- CustomElements.Button(
- onClick = {
- val i = Intent(ctx, ChatActivity::class.java)
- val b = Bundle()
- b.putString("aor", aor)
- b.putString("peer", message.peerUri)
- i.putExtras(b)
- chatRequest.launch(i)
- },
- onLongClick = {
- val peer = Utils.friendlyUri(ctx, message.peerUri, account)
- val contactExists =
- Contact.nameExists(peer, BaresipService.contacts, false)
- if (contactExists) {
- dialogMessage.value = String.format(
- getString(R.string.short_chat_question),
- peer
- )
- positiveButtonText.value = getString(R.string.delete)
- positiveAction.value = {
- Message.deleteAorPeerMessages(aor, message.peerUri)
- _uaMessages.value = uaMessages(aor)
- }
- neutralButtonText.value = ""
- } else {
- dialogMessage.value = String.format(
- getString(R.string.long_chat_question),
- peer
- )
- positiveButtonText.value = getString(R.string.add_contact)
- positiveAction.value = {
- val i = Intent(ctx, BaresipContactActivity::class.java)
- val b = Bundle()
- b.putBoolean("new", true)
- b.putString("uri", message.peerUri)
- i.putExtras(b)
- startActivity(i)
- }
- neutralButtonText.value = getString(R.string.delete)
- neutralAction.value = {
- Message.deleteAorPeerMessages(aor, message.peerUri)
- }
- }
- showDialog.value = true
- },
- modifier = Modifier
- .fillMaxWidth()
- .wrapContentHeight()
- .padding(end = 6.dp),
- shape = if (message.direction == MESSAGE_DOWN)
- RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp)
- else
- RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp),
- color =
- if (message.direction == MESSAGE_DOWN) {
- if (BaresipService.darkTheme.value)
- LocalCustomColors.current.secondaryDark
- else
- LocalCustomColors.current.secondaryLight
- } else {
- if (BaresipService.darkTheme.value)
- LocalCustomColors.current.primaryDark
- else
- LocalCustomColors.current.primaryLight
- },
- ) {
- val peer = Utils.friendlyUri(ctx, message.peerUri, account)
- val cal = GregorianCalendar()
- cal.timeInMillis = message.timeStamp
- val fmt: DateFormat = if (isToday(message.timeStamp))
- DateFormat.getTimeInstance(DateFormat.SHORT)
- else
- DateFormat.getDateInstance(DateFormat.SHORT)
- val info = fmt.format(cal.time)
- Column {
- Row {
- val textColor =
- if (message.direction == MESSAGE_DOWN) {
- if (BaresipService.darkTheme.value)
- LocalCustomColors.current.secondaryLight
- else
- LocalCustomColors.current.secondaryDark
- } else {
- if (BaresipService.darkTheme.value)
- LocalCustomColors.current.primaryLight
- else
- LocalCustomColors.current.primaryDark
- }
- Text(text = peer, color = textColor, fontSize = 12.sp)
- Spacer(modifier = Modifier.weight(1f))
- Text(text = info, color = textColor, fontSize = 12.sp)
- }
- Row {
- BasicText(
- text = message.message,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- style = TextStyle(
- color = LocalCustomColors.current.itemText,
- fontWeight = if (message.direction == MESSAGE_DOWN && message.new)
- FontWeight.Bold else FontWeight.Normal,
- fontSize = 16.sp
- )
- )
- }
- }
- }
- }
- }
- }
- }
-
- @Composable
- fun NewChatPeer() {
-
- val showDialog = remember { mutableStateOf(false) }
- val items = remember { mutableStateOf(listOf()) }
- val itemAction = remember { mutableStateOf<(Int) -> Unit>({ _ -> run {} }) }
-
- SelectableAlertDialog(
- openDialog = showDialog,
- title = stringResource(R.string.choose_destination_uri),
- items = items.value,
- onItemClicked = itemAction.value,
- neutralButtonText = stringResource(R.string.cancel),
- onNeutralClicked = {}
- )
-
- val suggestions by remember { contactNames }
- var filteredSuggestions by remember { mutableStateOf(suggestions) }
- var showSuggestions by remember { mutableStateOf(false) }
- val lazyListState = rememberLazyListState()
- val focusManager = LocalFocusManager.current
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .navigationBarsPadding()
- .padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- var newPeer by remember { mutableStateOf("") }
- Column(
- horizontalAlignment = Alignment.Start,
- modifier = Modifier.weight(1f)
- ) {
- if (showSuggestions && filteredSuggestions.isNotEmpty()) {
- Column(
- modifier = Modifier
- .shadow(8.dp, RoundedCornerShape(8.dp))
- .background(
- LocalCustomColors.current.grayLight,
- shape = RoundedCornerShape(8.dp)
- )
- .animateContentSize()
- ) {
- Box(modifier = Modifier
- .fillMaxWidth()
- .heightIn(max = 150.dp)
- ) {
- LazyColumn(
- modifier = Modifier
- .fillMaxWidth()
- .heightIn(max = 180.dp)
- .verticalScrollbar(
- state = lazyListState,
- color = LocalCustomColors.current.gray
- ),
- horizontalAlignment = Alignment.Start,
- state = lazyListState
- ) {
- items(
- items = filteredSuggestions,
- key = { suggestion -> suggestion }
- ) { suggestion ->
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- newPeer = suggestion
- showSuggestions = false
- }
- .padding(12.dp)
- ) {
- Text(
- text = suggestion,
- modifier = Modifier.fillMaxWidth(),
- color = LocalCustomColors.current.grayDark,
- fontSize = 18.sp
- )
- }
- }
- }
- }
- }
- Spacer(modifier = Modifier.height(8.dp))
- }
- OutlinedTextField(
- value = newPeer,
- placeholder = {
- Text(stringResource(R.string.new_chat_peer))
- },
- onValueChange = {
- newPeer = it
- showSuggestions = newPeer.length > 2
- filteredSuggestions = if (it.isEmpty()) {
- suggestions
- } else {
- suggestions.filter { suggestion ->
- newPeer.length > 2 && suggestion.startsWith(
- newPeer,
- ignoreCase = true
- )
- }
- }
- },
- modifier = Modifier
- .padding(end = 6.dp)
- .fillMaxWidth(),
- singleLine = true,
- trailingIcon = {
- if (newPeer.isNotEmpty())
- Icon(
- Icons.Outlined.Clear,
- contentDescription = null,
- modifier = Modifier
- .clickable {
- if (showSuggestions)
- showSuggestions = false
- else
- newPeer = ""
- }
- )
- },
- label = {
- LabelText(stringResource(R.string.new_chat_peer))
- },
- textStyle = TextStyle(
- fontSize = 18.sp,
- color = LocalCustomColors.current.itemText
- ),
- keyboardOptions = KeyboardOptions(
- keyboardType = KeyboardType.Text,
- imeAction = ImeAction.Done
- )
- )
- }
- Spacer(Modifier.width(4.dp))
- SmallFloatingActionButton(
- modifier = Modifier.padding(end = 4.dp),
- onClick = {
- showSuggestions = false
- val peerText = newPeer.trim()
- if (peerText.isNotEmpty()) {
- val uris = Contact.contactUris(peerText)
- if (uris.isEmpty())
- makeChat(peerText)
- else if (uris.size == 1)
- makeChat(uris[0])
- else {
- items.value = uris
- itemAction.value = { index ->
- makeChat(uris[index])
- }
- showDialog.value = true
- }
- }
- newPeer = ""
- focusManager.clearFocus()
- },
- containerColor = LocalCustomColors.current.accent,
- contentColor = LocalCustomColors.current.background
- ) {
- Icon(
- imageVector = Icons.Filled.Add,
- modifier = Modifier.size(36.dp),
- contentDescription = stringResource(R.string.add)
- )
- }
- }
- }
-
- private fun makeChat(chatPeer: String) {
- val peerUri = if (Utils.isTelNumber(chatPeer))
- "tel:$chatPeer"
- else
- chatPeer
- val uri = if (Utils.isTelUri(peerUri)) {
- if (account.telProvider == "") {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value =
- String.format(getString(R.string.no_telephony_provider), account.aor)
- showAlert.value = true
- ""
- } else
- Utils.telToSip(peerUri, account)
- } else
- Utils.uriComplete(peerUri, aor)
- if (alertMessage.value.isEmpty()) {
- if (!Utils.checkUri(uri)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_sip_or_tel_uri), uri)
- showAlert.value = true
- }
- else {
- val i = Intent(this@ChatsActivity, ChatActivity::class.java)
- val b = Bundle()
- b.putString("aor", aor)
- b.putString("peer", uri)
- i.putExtras(b)
- chatRequest.launch(i)
- }
- }
- }
-
- override fun onPause() {
- MainActivity.activityAor = aor
- super.onPause()
- }
-
- override fun onResume() {
- super.onResume()
- _uaMessages.value = uaMessages(aor)
- }
-
- override fun onDestroy() {
- if (Build.VERSION.SDK_INT >= 33) {
- if (backInvokedCallback != null)
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
- }
- else
- onBackPressedCallback.remove()
- super.onDestroy()
- }
-
- private fun goBack() {
- BaresipService.activities.remove("chats,$aor")
- returnResult()
- }
-
- private fun returnResult() {
- setResult(RESULT_CANCELED, Intent())
- finish()
- }
-
- private fun uaMessages(aor: String) : List {
- val res = mutableListOf()
- account.unreadMessages = false
- for (m in BaresipService.messages.reversed()) {
- if (m.aor != aor) continue
- var found = false
- for (r in res)
- if (r.peerUri == m.peerUri) {
- found = true
- break
- }
- if (!found) {
- res.add(0, m)
- if (m.new)
- account.unreadMessages = true
- }
- }
- return res.toList()
- }
-}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/ChatsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/ChatsScreen.kt
new file mode 100644
index 00000000..f6f760c8
--- /dev/null
+++ b/app/src/main/kotlin/com/tutpro/baresip/ChatsScreen.kt
@@ -0,0 +1,684 @@
+package com.tutpro.baresip
+
+import android.content.Context
+import android.graphics.BitmapFactory
+import android.text.format.DateUtils.isToday
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.outlined.Clear
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SmallFloatingActionButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import coil.compose.AsyncImage
+import com.tutpro.baresip.BaresipService.Companion.contactNames
+import com.tutpro.baresip.CustomElements.AlertDialog
+import com.tutpro.baresip.CustomElements.DropdownMenu
+import com.tutpro.baresip.CustomElements.LabelText
+import com.tutpro.baresip.CustomElements.SelectableAlertDialog
+import com.tutpro.baresip.CustomElements.verticalScrollbar
+import java.text.DateFormat
+import java.util.GregorianCalendar
+
+fun NavGraphBuilder.chatsScreenRoute(navController: NavController) {
+ composable(
+ route = "chats/{aor}",
+ arguments = listOf(navArgument("aor") { type = NavType.StringType })
+ ) { backStackEntry ->
+ val aor = backStackEntry.arguments?.getString("aor")!!
+ ChatsScreen(navController, aor)
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun ChatsScreen(navController: NavController, aor: String) {
+
+ val account = Account.ofAor(aor)!!
+ val ctx = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ val uaMessages: MutableState> = remember { mutableStateOf(emptyList()) }
+
+ DisposableEffect(lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_RESUME) {
+ Log.d(TAG, "Resumed to ChatsScreen for AOR: $aor")
+ uaMessages.value = loadMessages(account)
+ }
+ }
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
+ var areMessagesLoaded by remember { mutableStateOf(false) }
+ LaunchedEffect(aor) {
+ uaMessages.value = loadMessages(account)
+ areMessagesLoaded = true
+ }
+
+ Scaffold(
+ modifier = Modifier
+ .fillMaxSize()
+ .imePadding(),
+ containerColor = LocalCustomColors.current.background,
+ topBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
+ ) {
+ TopAppBar(navController, account, uaMessages)
+ }
+ },
+ bottomBar = { NewChatPeer(ctx, navController, account) },
+ content = { contentPadding ->
+ if (areMessagesLoaded)
+ ChatsContent(LocalContext.current, navController, contentPadding, account, uaMessages)
+ },
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TopAppBar(
+ navController: NavController,
+ account: Account,
+ uaMessages: MutableState>
+) {
+
+ var menuExpanded by remember { mutableStateOf(false) }
+ val delete = stringResource(R.string.delete)
+ val showDialog = remember { mutableStateOf(false) }
+ val positiveAction = remember { mutableStateOf({}) }
+
+ AlertDialog(
+ showDialog = showDialog,
+ title = stringResource(R.string.confirmation),
+ message = String.format(
+ stringResource(R.string.delete_chats_alert),
+ if (account.nickName.value != "")
+ account.nickName.value
+ else
+ account.aor.substringAfter(":")
+ ),
+ positiveButtonText = stringResource(R.string.delete),
+ onPositiveClicked = positiveAction.value,
+ negativeButtonText = stringResource(R.string.cancel),
+ )
+
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(R.string.chats),
+ color = LocalCustomColors.current.light,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = { navController.popBackStack() }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = null,
+ tint = LocalCustomColors.current.light
+ )
+ }
+ },
+ windowInsets = WindowInsets(0, 0, 0, 0),
+ actions = {
+ IconButton(
+ onClick = { menuExpanded = !menuExpanded }
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Menu,
+ contentDescription = "Menu",
+ tint = LocalCustomColors.current.light
+ )
+ }
+ DropdownMenu (
+ expanded = menuExpanded,
+ onDismissRequest = { menuExpanded = false },
+ items = listOf(delete),
+ onItemClick = { selectedItem ->
+ menuExpanded = false
+ when (selectedItem) {
+ delete -> {
+ positiveAction.value = {
+ Message.clearMessagesOfAor(account.aor)
+ Message.save()
+ uaMessages.value = listOf()
+ account.unreadMessages = false
+ }
+ showDialog.value = true
+ }
+ }
+ }
+ )
+ },
+ colors = TopAppBarDefaults.mediumTopAppBarColors(
+ containerColor = LocalCustomColors.current.primary
+ )
+ )
+}
+
+@Composable
+private fun ChatsContent(
+ ctx: Context,
+ navController: NavController,
+ contentPadding: PaddingValues,
+ account: Account,
+ uaMessages: MutableState>
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(contentPadding),
+ verticalArrangement = Arrangement.Top
+ ) {
+ Account(account)
+ Chats(ctx, navController, account, uaMessages)
+ }
+}
+
+@Composable
+private fun Account(account: Account) {
+ val headerText = stringResource(R.string.account) + " " +
+ if (account.nickName.value != "")
+ account.nickName.value
+ else
+ account.aor.substringAfter(":")
+ Text(
+ text = headerText,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, bottom = 8.dp),
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.SemiBold,
+ textAlign = TextAlign.Center
+ )
+}
+
+@Composable
+private fun Chats(
+ ctx: Context,
+ navController: NavController,
+ account: Account,
+ uaMessages: MutableState>
+) {
+ val aor = account.aor
+
+ val showDialog = remember { mutableStateOf(false) }
+ val dialogMessage = remember { mutableStateOf("") }
+ val positiveButtonText = remember { mutableStateOf("") }
+ val positiveAction = remember { mutableStateOf({}) }
+ val neutralButtonText = remember { mutableStateOf("") }
+ val neutralAction = remember { mutableStateOf({}) }
+
+ if (showDialog.value)
+ AlertDialog(
+ showDialog = showDialog,
+ title = stringResource(R.string.confirmation),
+ message = dialogMessage.value,
+ positiveButtonText = positiveButtonText.value,
+ onPositiveClicked = positiveAction.value,
+ neutralButtonText = neutralButtonText.value,
+ onNeutralClicked = neutralAction.value,
+ negativeButtonText = stringResource(R.string.cancel)
+ )
+
+ val lazyListState = rememberLazyListState()
+
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 8.dp, end = 4.dp)
+ .verticalScrollbar(
+ state = lazyListState,
+ width = 4.dp,
+ color = LocalCustomColors.current.gray
+ )
+ .background(LocalCustomColors.current.background),
+ reverseLayout = true,
+ state = lazyListState,
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ items(items = uaMessages.value, key = { message -> message.timeStamp }) { message ->
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ when (val contact = Contact.findContact(message.peerUri)) {
+ is Contact.BaresipContact -> {
+ val avatarImage = contact.avatarImage
+ if (avatarImage != null)
+ CustomElements.ImageAvatar(avatarImage)
+ else
+ CustomElements.TextAvatar(contact.name, contact.color)
+ }
+
+ is Contact.AndroidContact -> {
+ val thumbNailUri = contact.thumbnailUri
+ if (thumbNailUri != null)
+ AsyncImage(
+ model = thumbNailUri,
+ contentDescription = "Avatar",
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .size(36.dp)
+ .clip(CircleShape),
+ )
+ else
+ CustomElements.TextAvatar(contact.name, contact.color)
+ }
+
+ null -> {
+ val avatarImage = BitmapFactory
+ .decodeResource(ctx.resources, R.drawable.person_image)
+ CustomElements.ImageAvatar(avatarImage)
+ }
+ }
+ Spacer(modifier = Modifier.width(6.dp))
+ val buttonShape = if (message.direction == MESSAGE_DOWN) {
+ RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp)
+ } else {
+ RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp)
+ }
+ val borderStroke = if (account.unreadMessages && Message.unreadMessagesFromPeer(aor, message.peerUri)) {
+ BorderStroke(width = 2.dp, color = LocalCustomColors.current.alert)
+ } else {
+ null
+ }
+ CustomElements.Button(
+ onClick = {
+ navController.navigate("chat/${aor}/${message.peerUri}")
+ },
+ onLongClick = {
+ val peer = Utils.friendlyUri(ctx, message.peerUri, account)
+ val contactExists =
+ Contact.nameExists(peer, BaresipService.contacts, false)
+ if (contactExists) {
+ dialogMessage.value = String.format(
+ ctx.getString(R.string.short_chat_question),
+ peer
+ )
+ positiveButtonText.value = ctx.getString(R.string.delete)
+ positiveAction.value = {
+ Message.deleteAorPeerMessages(aor, message.peerUri)
+ uaMessages.value = loadMessages(account)
+ }
+ neutralButtonText.value = ""
+ } else {
+ dialogMessage.value =
+ String.format(ctx.getString(R.string.long_chat_question), peer)
+ positiveButtonText.value = ctx.getString(R.string.add_contact)
+ positiveAction.value = {
+ navController.navigate("baresip_contact/${message.peerUri}/new")
+ }
+ neutralButtonText.value = ctx.getString(R.string.delete)
+ neutralAction.value = {
+ Message.deleteAorPeerMessages(aor, message.peerUri)
+ }
+ }
+ showDialog.value = true
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(end = 6.dp),
+ shape = buttonShape,
+ border = borderStroke,
+ color =
+ if (message.direction == MESSAGE_DOWN) {
+ if (BaresipService.darkTheme.value)
+ LocalCustomColors.current.secondaryDark
+ else
+ LocalCustomColors.current.secondaryLight
+ } else {
+ if (BaresipService.darkTheme.value)
+ LocalCustomColors.current.primaryDark
+ else
+ LocalCustomColors.current.primaryLight
+ },
+ ) {
+ val peer = Utils.friendlyUri(ctx, message.peerUri, account)
+ val cal = GregorianCalendar()
+ cal.timeInMillis = message.timeStamp
+ val fmt: DateFormat = if (isToday(message.timeStamp))
+ DateFormat.getTimeInstance(DateFormat.SHORT)
+ else
+ DateFormat.getDateInstance(DateFormat.SHORT)
+ val info = fmt.format(cal.time)
+ Column {
+ Row {
+ val textColor =
+ if (message.direction == MESSAGE_DOWN) {
+ if (BaresipService.darkTheme.value)
+ LocalCustomColors.current.secondaryLight
+ else
+ LocalCustomColors.current.secondaryDark
+ } else {
+ if (BaresipService.darkTheme.value)
+ LocalCustomColors.current.primaryLight
+ else
+ LocalCustomColors.current.primaryDark
+ }
+ Text(text = peer, color = textColor, fontSize = 12.sp)
+ Spacer(modifier = Modifier.weight(1f))
+ Text(text = info, color = textColor, fontSize = 12.sp)
+ }
+ Row {
+ BasicText(
+ text = message.message,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = TextStyle(
+ color = LocalCustomColors.current.itemText,
+ fontWeight = if (message.direction == MESSAGE_DOWN && message.new)
+ FontWeight.Bold else FontWeight.Normal,
+ fontSize = 16.sp
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun NewChatPeer(ctx: Context, navController: NavController, account: Account) {
+
+ val alertTitle = remember { mutableStateOf("") }
+ val alertMessage = remember { mutableStateOf("") }
+ val showAlert = remember { mutableStateOf(false) }
+
+ fun makeChat(ctx: Context, navController: NavController, account: Account, chatPeer: String) {
+ val peerUri = if (Utils.isTelNumber(chatPeer))
+ "tel:$chatPeer"
+ else
+ chatPeer
+ val uri = if (Utils.isTelUri(peerUri)) {
+ if (account.telProvider == "") {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value =
+ String.format(ctx.getString(R.string.no_telephony_provider), account.aor)
+ showAlert.value = true
+ ""
+ } else
+ Utils.telToSip(peerUri, account)
+ } else
+ Utils.uriComplete(peerUri, account.aor)
+ if (alertMessage.value.isEmpty()) {
+ if (!Utils.checkUri(uri)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri)
+ showAlert.value = true
+ }
+ else
+ navController.navigate("chat/${account.aor}/${uri}")
+ }
+ }
+
+ if (showAlert.value) {
+ AlertDialog(
+ showDialog = showAlert,
+ title = alertTitle.value,
+ message = alertMessage.value,
+ positiveButtonText = stringResource(R.string.ok),
+ )
+ }
+
+ val showDialog = remember { mutableStateOf(false) }
+ val items = remember { mutableStateOf(listOf()) }
+ val itemAction = remember { mutableStateOf<(Int) -> Unit>({ _ -> run {} }) }
+
+ SelectableAlertDialog(
+ openDialog = showDialog,
+ title = stringResource(R.string.choose_destination_uri),
+ items = items.value,
+ onItemClicked = itemAction.value,
+ neutralButtonText = stringResource(R.string.cancel),
+ onNeutralClicked = {}
+ )
+
+ val suggestions by remember { contactNames }
+ var filteredSuggestions by remember { mutableStateOf(suggestions) }
+ var showSuggestions by remember { mutableStateOf(false) }
+ val lazyListState = rememberLazyListState()
+ val focusManager = LocalFocusManager.current
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .navigationBarsPadding()
+ .padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ var newPeer by remember { mutableStateOf("") }
+ Column(
+ horizontalAlignment = Alignment.Start,
+ modifier = Modifier.weight(1f)
+ ) {
+ if (showSuggestions && filteredSuggestions.isNotEmpty()) {
+ Column(
+ modifier = Modifier
+ .shadow(8.dp, RoundedCornerShape(8.dp))
+ .background(
+ LocalCustomColors.current.grayLight,
+ shape = RoundedCornerShape(8.dp)
+ )
+ .animateContentSize()
+ ) {
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(max = 150.dp)
+ ) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(max = 180.dp)
+ .verticalScrollbar(
+ state = lazyListState,
+ color = LocalCustomColors.current.gray
+ ),
+ horizontalAlignment = Alignment.Start,
+ state = lazyListState
+ ) {
+ items(
+ items = filteredSuggestions,
+ key = { suggestion -> suggestion }
+ ) { suggestion ->
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ newPeer = suggestion
+ showSuggestions = false
+ }
+ .padding(12.dp)
+ ) {
+ Text(
+ text = suggestion,
+ modifier = Modifier.fillMaxWidth(),
+ color = LocalCustomColors.current.grayDark,
+ fontSize = 18.sp
+ )
+ }
+ }
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+ OutlinedTextField(
+ value = newPeer,
+ placeholder = { Text(stringResource(R.string.new_chat_peer)) },
+ onValueChange = {
+ newPeer = it
+ showSuggestions = newPeer.length > 2
+ filteredSuggestions = if (it.isEmpty()) {
+ suggestions
+ } else {
+ suggestions.filter { suggestion ->
+ newPeer.length > 2 && suggestion.startsWith(
+ newPeer,
+ ignoreCase = true
+ )
+ }
+ }
+ },
+ modifier = Modifier
+ .padding(end = 6.dp)
+ .fillMaxWidth(),
+ singleLine = true,
+ trailingIcon = {
+ if (newPeer.isNotEmpty())
+ Icon(
+ Icons.Outlined.Clear,
+ contentDescription = null,
+ modifier = Modifier
+ .clickable {
+ if (showSuggestions)
+ showSuggestions = false
+ else
+ newPeer = ""
+ }
+ )
+ },
+ label = {
+ LabelText(stringResource(R.string.new_chat_peer))
+ },
+ textStyle = TextStyle(
+ fontSize = 18.sp,
+ color = LocalCustomColors.current.itemText
+ ),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ imeAction = ImeAction.Done
+ )
+ )
+ }
+ Spacer(Modifier.width(4.dp))
+ SmallFloatingActionButton(
+ modifier = Modifier.padding(end = 4.dp),
+ onClick = {
+ showSuggestions = false
+ val peerText = newPeer.trim()
+ if (peerText.isNotEmpty()) {
+ val uris = Contact.contactUris(peerText)
+ if (uris.isEmpty())
+ makeChat(ctx, navController, account, peerText)
+ else if (uris.size == 1)
+ makeChat(ctx, navController, account, uris[0])
+ else {
+ items.value = uris
+ itemAction.value = { index ->
+ makeChat(ctx, navController, account, uris[index])
+ }
+ showDialog.value = true
+ }
+ }
+ newPeer = ""
+ focusManager.clearFocus()
+ },
+ containerColor = LocalCustomColors.current.accent,
+ contentColor = LocalCustomColors.current.background
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Add,
+ modifier = Modifier.size(36.dp),
+ contentDescription = stringResource(R.string.add)
+ )
+ }
+ }
+}
+
+private fun loadMessages(account: Account) : List {
+ val res = mutableListOf()
+ account.unreadMessages = false
+ for (m in BaresipService.messages.reversed()) {
+ if (m.aor != account.aor) continue
+ var found = false
+ for (r in res)
+ if (r.peerUri == m.peerUri) {
+ found = true
+ break
+ }
+ if (!found) {
+ res.add(0, m)
+ if (m.new)
+ account.unreadMessages = true
+ }
+ }
+ return res.toList()
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/tutpro/baresip/CodecsActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/CodecsActivity.kt
deleted file mode 100644
index efd0390c..00000000
--- a/app/src/main/kotlin/com/tutpro/baresip/CodecsActivity.kt
+++ /dev/null
@@ -1,384 +0,0 @@
-package com.tutpro.baresip
-
-import android.os.Build
-import android.os.Bundle
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.ListItem
-import androidx.compose.material3.ListItemDefaults
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.snapshots.SnapshotStateList
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.res.vectorResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import com.tutpro.baresip.CustomElements.AlertDialog
-import com.tutpro.baresip.CustomElements.verticalScrollbar
-
-class CodecsActivity : ComponentActivity() {
-
- private lateinit var acc: Account
- private lateinit var ua: UserAgent
-
- private lateinit var allCodecs: List
- private lateinit var accCodecs: List
- private lateinit var codecs: SnapshotStateList
-
- private var aor = ""
- private var media = ""
- private var title = ""
-
- private val alertTitle = mutableStateOf("")
- private val alertMessage = mutableStateOf("")
- private val showAlert = mutableStateOf(false)
-
- private var backInvokedCallback: OnBackInvokedCallback? = null
- private lateinit var onBackPressedCallback: OnBackPressedCallback
-
- @RequiresApi(33)
- private fun registerBackInvokedCallback() {
- backInvokedCallback = OnBackInvokedCallback { goBack() }
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- backInvokedCallback!!
- )
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
-
- super.onCreate(savedInstanceState)
-
- enableEdgeToEdge()
-
- if (Build.VERSION.SDK_INT >= 33)
- registerBackInvokedCallback()
- else {
- onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- goBack()
- }
- }
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
- aor = intent.getStringExtra("aor")!!
- media = intent.getStringExtra("media")!!
-
- Utils.addActivity("codecs,$aor,$media")
-
- ua = UserAgent.ofAor(aor)!!
- acc = ua.account
-
- if (media == "audio") {
- title = getString(R.string.audio_codecs)
- allCodecs = ArrayList(Api.audio_codecs().split(","))
- accCodecs = acc.audioCodec
- } else {
- title = getString(R.string.video_codecs)
- allCodecs = ArrayList(Api.video_codecs().split(",").distinct())
- accCodecs = acc.videoCodec
- }
-
- val currentCodecs = mutableListOf()
- for (codec in accCodecs)
- currentCodecs.add(Codec(codec, mutableStateOf(true)))
- for (codec in allCodecs)
- if (codec !in accCodecs)
- currentCodecs.add(Codec(codec, mutableStateOf(false)))
-
- codecs = mutableStateListOf().apply { addAll(currentCodecs) }
-
- setContent {
- AppTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = LocalCustomColors.current.background
- ) {
- CodecsScreen { goBack() }
- }
- }
- }
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- fun CodecsScreen(navigateBack: () -> Unit) {
- Scaffold(
- modifier = Modifier
- .fillMaxHeight()
- .imePadding()
- .safeDrawingPadding(),
- containerColor = LocalCustomColors.current.background,
- topBar = {
- TopAppBar(
- title = {
- Text(
- text = stringResource(R.string.codecs),
- color = LocalCustomColors.current.light,
- fontWeight = FontWeight.Bold
- )
- },
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = LocalCustomColors.current.primary
- ),
- navigationIcon = {
- IconButton(onClick = navigateBack) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = LocalCustomColors.current.light
- )
- }
- },
- actions = {
- IconButton(onClick = {
- updateCodecs()
- }) {
- Icon(
- imageVector = Icons.Filled.Check,
- tint = LocalCustomColors.current.light,
- contentDescription = "Check"
- )
- }
- }
- )
- },
- content = { contentPadding ->
- CodecsContent(contentPadding)
- },
- )
- }
-
- @Composable
- fun CodecsContent(contentPadding: PaddingValues) {
-
- if (showAlert.value) {
- AlertDialog(
- showDialog = showAlert,
- title = alertTitle.value,
- message = alertMessage.value,
- positiveButtonText = stringResource(R.string.ok),
- )
- }
-
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .background(LocalCustomColors.current.background)
- .padding(contentPadding)
- .padding(bottom = 16.dp),
- ) {
- Title()
- Codecs(codecs = codecs, onCodecsChange = { updatedCodecs ->
- onCodecsChange(updatedCodecs)
- })
- }
- }
-
- private fun onCodecsChange(updatedCodecs: List) {
- codecs.clear()
- codecs.addAll(updatedCodecs)
- }
-
- @Composable
- fun Title() {
- Text(
- text = title,
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 8.dp)
- .clickable {
- if (media == "audio") {
- alertTitle.value = getString(R.string.audio_codecs)
- alertMessage.value = getString(R.string.audio_codecs_help)
- }
- else {
- alertTitle.value = getString(R.string.video_codecs)
- alertMessage.value = getString(R.string.video_codecs_help)
- }
- showAlert.value = true
- },
- fontSize = 18.sp,
- fontWeight = FontWeight.SemiBold,
- color = LocalCustomColors.current.itemText,
- textAlign = TextAlign.Center
- )
- }
-
- @OptIn(ExperimentalFoundationApi::class)
- @Composable
- fun Codecs(codecs: SnapshotStateList, onCodecsChange: (List) -> Unit) {
-
- val draggableState = rememberDraggableListState(
- onMove = { fromIndex, toIndex ->
- codecs.add(toIndex, codecs.removeAt(fromIndex))
- onCodecsChange(codecs.toList())
- }
- )
-
- LazyColumn(
- modifier = Modifier
- .padding(end = 4.dp)
- .verticalScrollbar(
- state = draggableState.listState,
- width = 4.dp,
- color = LocalCustomColors.current.gray
- ),
- state = draggableState.listState,
- contentPadding = PaddingValues(start = 12.dp, end = 12.dp),
- //verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- draggableItems(
- state = draggableState,
- items = codecs,
- key = { item -> item.name }
- ) { item, isDragging ->
- ListItem(
- colors = ListItemDefaults.colors(
- containerColor = if (isDragging)
- LocalCustomColors.current.grayLight
- else
- LocalCustomColors.current.background
- ),
- headlineContent = {
- Text(text = item.name,
- modifier = Modifier.fillMaxWidth()
- .alpha(if (item.enabled.value) 1.0f else 0.5f)
- .padding(start = 6.dp)
- .combinedClickable(
- onClick = {},
- onLongClick = {
- item.enabled.value = !item.enabled.value
- if (item.enabled.value) {
- val index = codecs.indexOf(item)
- codecs.removeAt(index)
- codecs.add(0, item)
- }
- else {
- val index = codecs.indexOf(item)
- codecs.removeAt(index)
- codecs.add(item)
- }
- onCodecsChange(codecs.toList())
- }
- )
- )
- },
- trailingContent = {
- Icon(
- modifier = Modifier.dragHandle(
- state = draggableState,
- key = item.name
- ),
- imageVector =ImageVector.vectorResource(R.drawable.reorder),
- contentDescription = null
- )
- },
- )
- if (codecs.indexOf(item) > 0)
- HorizontalDivider(
- color = LocalCustomColors.current.gray,
- modifier = Modifier.padding(horizontal = 12.dp),
- thickness = 1.dp
- )
- }
- }
- }
-
- private fun updateCodecs() {
-
- var save = false
- val newCodecs = ArrayList()
-
- for (codec in codecs)
- if (codec.enabled.value)
- newCodecs.add(codec.name)
-
- val codecList = Utils.implode(newCodecs, ",")
-
- if (media == "audio")
- if (newCodecs != acc.audioCodec) {
- if (Api.account_set_audio_codecs(acc.accp, codecList) == 0) {
- Log.d(TAG, "New audio codecs '$codecList'")
- acc.audioCodec = newCodecs
- save = true
- } else {
- Log.e(TAG, "Setting of audio codecs '$codecList' failed")
- }
- }
-
- if (media == "video")
- if (newCodecs != acc.videoCodec) {
- if (Api.account_set_video_codecs(acc.accp, codecList) == 0) {
- Log.d(TAG, "New video codecs '$codecs'")
- acc.videoCodec = newCodecs
- save = true
- } else {
- Log.e(TAG, "Setting of video codecs '$codecs' failed")
- }
- }
-
- if (save)
- AccountsActivity.saveAccounts()
-
- BaresipService.activities.remove("codecs,$aor,$media")
- finish()
- }
-
- private fun goBack() {
- BaresipService.activities.remove("codecs,$aor,$media")
- finish()
- }
-
- override fun onPause() {
- MainActivity.activityAor = aor
- super.onPause()
- }
-
- override fun onDestroy() {
- if (Build.VERSION.SDK_INT >= 33) {
- if (backInvokedCallback != null)
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
- }
- else
- onBackPressedCallback.remove()
- super.onDestroy()
- }
-}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/CodecsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/CodecsScreen.kt
new file mode 100644
index 00000000..63b2c36d
--- /dev/null
+++ b/app/src/main/kotlin/com/tutpro/baresip/CodecsScreen.kt
@@ -0,0 +1,301 @@
+package com.tutpro.baresip
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import com.tutpro.baresip.CustomElements.AlertDialog
+import com.tutpro.baresip.CustomElements.verticalScrollbar
+
+fun NavGraphBuilder.codecsScreenRoute(navController: NavController) {
+ composable(
+ route = "codecs/{aor}/{media}",
+ arguments = listOf(
+ navArgument("aor") { type = NavType.StringType },
+ navArgument("media") { type = NavType.StringType })
+ ) { backStackEntry ->
+ val aor = backStackEntry.arguments?.getString("aor")!!
+ val media = backStackEntry.arguments?.getString("media")!!
+ val account = UserAgent.ofAor(aor)?.account!!
+ CodecsScreen(
+ onBack = { navController.popBackStack() },
+ checkOnClick = { updatedCodecs ->
+ val enabledCodecNames = updatedCodecs.filter { it.enabled.value }.map { it.name }
+ val codecList = Utils.implode(enabledCodecNames, ",")
+ Log.d(TAG, "Saving codecs for ${account.aor} (${media}): $codecList")
+ val success = if (media == "audio")
+ Api.account_set_audio_codecs(account.accp, codecList)
+ else
+ Api.account_set_video_codecs(account.accp, codecList)
+ if (success == 0) {
+ if (media == "audio")
+ account.audioCodec = enabledCodecNames as ArrayList
+ else
+ account.videoCodec = enabledCodecNames as ArrayList
+ Account.saveAccounts()
+ Log.d("CodecsSave", "Codecs saved successfully.")
+ }
+ else
+ Log.e(TAG, "Failed to set $aor codecs.")
+ navController.popBackStack()
+ },
+ aor = aor,
+ media = media
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun CodecsScreen(
+ onBack: () -> Unit,
+ checkOnClick: (List) -> Unit,
+ aor: String,
+ media: String
+) {
+ val ua = UserAgent.ofAor(aor)!!
+ val acc = ua.account
+ var currentCodecsState by remember { mutableStateOf>(emptyList()) }
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize().imePadding(),
+ containerColor = LocalCustomColors.current.background,
+ topBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(
+ top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
+ )
+ ) {
+ TopAppBar(
+ title = {
+ Text(
+ text = if (media == "audio")
+ stringResource(R.string.audio_codecs)
+ else
+ stringResource(R.string.video_codecs),
+ color = LocalCustomColors.current.light,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ colors = TopAppBarDefaults.mediumTopAppBarColors(
+ containerColor = LocalCustomColors.current.primary
+ ),
+ windowInsets = WindowInsets(0, 0, 0, 0),
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back",
+ tint = LocalCustomColors.current.light
+ )
+ }
+ },
+ actions = {
+ IconButton(onClick = {
+ checkOnClick(currentCodecsState)
+ }) {
+ Icon(
+ imageVector = Icons.Filled.Check,
+ tint = LocalCustomColors.current.light,
+ contentDescription = "Check"
+ )
+ }
+ }
+ )
+ }
+ },
+ content = { contentPadding ->
+ CodecsContent(
+ contentPadding,
+ acc,
+ media,
+ onUpdateCodecs = { updatedCodecs -> currentCodecsState = updatedCodecs }
+ )
+ },
+ )
+}
+
+private val alertTitle = mutableStateOf("")
+private val alertMessage = mutableStateOf("")
+private val showAlert = mutableStateOf(false)
+
+
+@Composable
+private fun CodecsContent(
+ contentPadding: PaddingValues,
+ acc: Account,
+ media: String,
+ onUpdateCodecs: (List) -> Unit
+) {
+ val codecs = remember { mutableStateListOf() }
+
+ LaunchedEffect(acc, media) {
+ val allCodecs: List = if (media == "audio") {
+ Api.audio_codecs().split(",")
+ } else {
+ Api.video_codecs().split(",").distinct()
+ }
+ val accCodecs: List = if (media == "audio") {
+ acc.audioCodec
+ } else {
+ acc.videoCodec
+ }
+ val currentCodecs = mutableListOf()
+ for (codec in accCodecs)
+ currentCodecs.add(Codec(codec, mutableStateOf(true)))
+ for (codec in allCodecs)
+ if (codec !in accCodecs)
+ currentCodecs.add(Codec(codec, mutableStateOf(false)))
+ codecs.clear()
+ codecs.addAll(currentCodecs)
+ }
+
+ if (showAlert.value) {
+ AlertDialog(
+ showDialog = showAlert,
+ title = alertTitle.value,
+ message = alertMessage.value,
+ positiveButtonText = stringResource(R.string.ok),
+ )
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(contentPadding)
+ .padding(bottom = 16.dp),
+ ) {
+ Codecs(
+ codecs = codecs,
+ onUpdateCodecs = onUpdateCodecs
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun Codecs(codecs: SnapshotStateList, onUpdateCodecs: (List) -> Unit) {
+
+ val draggableState = rememberDraggableListState(
+ onMove = { fromIndex, toIndex ->
+ codecs.add(toIndex, codecs.removeAt(fromIndex))
+ onUpdateCodecs(codecs.toList())
+ }
+ )
+
+ LazyColumn(
+ modifier = Modifier
+ .padding(end = 4.dp)
+ .verticalScrollbar(
+ state = draggableState.listState,
+ width = 4.dp,
+ color = LocalCustomColors.current.gray
+ ),
+ state = draggableState.listState,
+ contentPadding = PaddingValues(start = 12.dp, end = 12.dp),
+ //verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ draggableItems(
+ state = draggableState,
+ items = codecs,
+ key = { item -> item.name }
+ ) { item, isDragging ->
+ ListItem(
+ colors = ListItemDefaults.colors(
+ containerColor = if (isDragging)
+ LocalCustomColors.current.grayLight
+ else
+ LocalCustomColors.current.background
+ ),
+ headlineContent = {
+ Text(text = item.name,
+ modifier = Modifier
+ .fillMaxWidth()
+ .alpha(if (item.enabled.value) 1.0f else 0.5f)
+ .padding(start = 6.dp)
+ .combinedClickable(
+ onClick = {},
+ onLongClick = {
+ item.enabled.value = !item.enabled.value
+ if (item.enabled.value) {
+ val index = codecs.indexOf(item)
+ codecs.removeAt(index)
+ codecs.add(0, item)
+ } else {
+ val index = codecs.indexOf(item)
+ codecs.removeAt(index)
+ codecs.add(item)
+ }
+ onUpdateCodecs(codecs.toList())
+ }
+ )
+ )
+ },
+ trailingContent = {
+ Icon(
+ modifier = Modifier.dragHandle(
+ state = draggableState,
+ key = item.name
+ ),
+ imageVector =ImageVector.vectorResource(R.drawable.reorder),
+ contentDescription = null
+ )
+ },
+ )
+ if (codecs.indexOf(item) > 0)
+ HorizontalDivider(
+ color = LocalCustomColors.current.gray,
+ modifier = Modifier.padding(horizontal = 12.dp),
+ thickness = 1.dp
+ )
+ }
+ }
+}
+
+
diff --git a/app/src/main/kotlin/com/tutpro/baresip/Config.kt b/app/src/main/kotlin/com/tutpro/baresip/Config.kt
index 4be7d1e7..1874d8fa 100644
--- a/app/src/main/kotlin/com/tutpro/baresip/Config.kt
+++ b/app/src/main/kotlin/com/tutpro/baresip/Config.kt
@@ -10,6 +10,7 @@ import java.nio.charset.StandardCharsets
object Config {
private val configPath = BaresipService.filesPath + "/config"
+ val audioModules = listOf("opus", "amr", "g722", "g7221", "g726", "g729", "codec2", "g711")
private lateinit var config: String
private lateinit var previousConfig: String
private lateinit var previousLines: List
@@ -18,7 +19,7 @@ object Config {
config = ctx.assets.open("config.static").bufferedReader().use { it.readText() }
if (!File(configPath).exists()) {
- for (module in AudioActivity.audioModules)
+ for (module in audioModules)
config = "${config}module ${module}.so\n"
previousConfig = config
} else {
@@ -149,7 +150,7 @@ object Config {
}
val previousModules = previousVariables("module")
- for (module in AudioActivity.audioModules)
+ for (module in audioModules)
if ("${module}.so" in previousModules)
config = "${config}module ${module}.so\n"
diff --git a/app/src/main/kotlin/com/tutpro/baresip/ConfigActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/ConfigActivity.kt
deleted file mode 100644
index bb9691da..00000000
--- a/app/src/main/kotlin/com/tutpro/baresip/ConfigActivity.kt
+++ /dev/null
@@ -1,1407 +0,0 @@
-package com.tutpro.baresip
-
-import android.Manifest
-import android.app.role.RoleManager
-import android.content.ActivityNotFoundException
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.media.RingtoneManager
-import android.net.Uri
-import android.os.Build.VERSION
-import android.os.Bundle
-import android.os.PowerManager
-import android.provider.Settings
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.activity.result.ActivityResult
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.annotation.RequiresApi
-import androidx.appcompat.app.AppCompatDelegate
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.Switch
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.core.content.ContextCompat
-import androidx.core.net.toUri
-import com.tutpro.baresip.CustomElements.AlertDialog
-import com.tutpro.baresip.CustomElements.LabelText
-import com.tutpro.baresip.CustomElements.verticalScrollbar
-import com.tutpro.baresip.Utils.copyInputStreamToFile
-import java.io.File
-import java.io.FileInputStream
-import java.util.Locale
-
-class ConfigActivity : ComponentActivity() {
-
- private lateinit var baresipService: Intent
- private var oldAutoStart = false
- private var newAutoStart = false
- private var oldDarkTheme = false
- private var newDarkTheme = false
- private var oldListenAddr = ""
- private var newListenAddr = ""
- private var oldAddressFamily = ""
- private var newAddressFamily = ""
- private var oldDnsServers = ""
- private var newDnsServers = ""
- private var oldTlsCertificateFile = false
- private var newTlsCertificateFile = false
- private var oldVerifyServer = false
- private var newVerifyServer = false
- private var oldCaFile = false
- private var newCaFile = false
- private var oldUserAgent = ""
- private var newUserAgent = ""
- private var oldRingtoneUri = ""
- private var newRingtoneUri = ""
- private var oldBatteryOptimizations = false
- private var newBatteryOptimizations = false
- private var oldDefaultDialer = false
- private var newDefaultDialer = false
- private var oldContactsMode = ""
- private var newContactsMode = ""
- private var oldDebug = false
- private var newDebug = false
- private var oldSipTrace = false
- private var newSipTrace = false
-
- private lateinit var roleManager: RoleManager
- private lateinit var powerManager: PowerManager
- private lateinit var dialerRoleRequest: ActivityResultLauncher
- private lateinit var androidSettingsRequest: ActivityResultLauncher
- private lateinit var requestPermissionLauncher: ActivityResultLauncher
- private lateinit var requestPermissionsLauncher: ActivityResultLauncher>
-
- private var save = false
- private var restart = false
- private var audioRestart = false
-
- private val alertTitle = mutableStateOf("")
- private val alertMessage = mutableStateOf("")
- private val showAlert = mutableStateOf(false)
-
- private val dialogTitle = mutableStateOf("")
- private val dialogMessage = mutableStateOf("")
- private val positiveText = mutableStateOf("")
- private val onPositiveClicked = mutableStateOf({})
- private val negativeText = mutableStateOf("")
- private val onNegativeClicked = mutableStateOf({})
- private val showDialog = mutableStateOf(false)
-
- private var backInvokedCallback: OnBackInvokedCallback? = null
- private lateinit var onBackPressedCallback: OnBackPressedCallback
-
- @RequiresApi(33)
- private fun registerBackInvokedCallback() {
- backInvokedCallback = OnBackInvokedCallback { goBack() }
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- backInvokedCallback!!
- )
- }
-
- private val certificateRequest =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- val certPath = BaresipService.filesPath + "/cert.pem"
- val certFile = File(certPath)
- if (it.resultCode == RESULT_OK) {
- it.data?.data?.also { uri ->
- try {
- val inputStream =
- applicationContext.contentResolver.openInputStream(uri)
- as FileInputStream
- certFile.copyInputStreamToFile(inputStream)
- inputStream.close()
- Config.replaceVariable("sip_certificate", certPath)
- save = true
- restart = true
- } catch (e: Error) {
- alertTitle.value = getString(R.string.error)
- alertMessage.value = getString(R.string.read_cert_error) + ": " + e.message
- showAlert.value = true
- newTlsCertificateFile = false
- }
- }
- }
- else
- newTlsCertificateFile = false
- if (!newTlsCertificateFile)
- Utils.deleteFile(certFile)
- }
-
- private val caCertsRequest =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- val caCertsFile = File(BaresipService.filesPath + "/ca_certs.crt")
- if (it.resultCode == RESULT_OK)
- it.data?.data?.also { uri ->
- try {
- val inputStream =
- applicationContext.contentResolver.openInputStream(uri)
- as FileInputStream
- caCertsFile.copyInputStreamToFile(inputStream)
- inputStream.close()
- restart = true
- } catch (e: Error) {
- alertTitle.value = getString(R.string.error)
- alertMessage.value = getString(R.string.read_ca_certs_error) + ": " + e.message
- showAlert.value = true
- newCaFile = false
- }
- }
- else
- newCaFile = false
- if (!newCaFile)
- caCertsFile.delete()
- }
-
- private val audioRequest = registerForActivityResult(
- ActivityResultContracts.StartActivityForResult()
- ) {
- audioRestart = it.resultCode == RESULT_OK
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
-
- super.onCreate(savedInstanceState)
-
- enableEdgeToEdge()
-
- if (VERSION.SDK_INT >= 33)
- registerBackInvokedCallback()
- else {
- onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- goBack()
- }
- }
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
- val title = getString(R.string.configuration)
-
- Utils.addActivity("config")
-
- baresipService = Intent(this@ConfigActivity, BaresipService::class.java)
-
- oldAutoStart = Config.variable("auto_start") == "yes"
- if (oldAutoStart && !isAppearOnTopPermissionGranted(this)) {
- Config.replaceVariable("auto_start", "no")
- oldAutoStart = false
- save = true
- }
- newAutoStart = oldAutoStart
-
- oldListenAddr = Config.variable("sip_listen")
-
- oldAddressFamily = Config.variable("net_af").lowercase()
- newAddressFamily = oldAddressFamily
-
- val dynamicDns = Config.variable("dyn_dns")
- if (dynamicDns == "yes") {
- oldDnsServers = ""
- } else {
- val servers = Config.variables("dns_server")
- var serverList = ""
- for (server in servers)
- serverList += ", $server"
- oldDnsServers = serverList.trimStart(',').trimStart(' ')
- }
-
- val certFile = File(BaresipService.filesPath + "/cert.pem")
- oldTlsCertificateFile = certFile.exists()
-
- oldVerifyServer = Config.variable("sip_verify_server") == "yes"
- newVerifyServer = oldVerifyServer
-
- val caCertsFile = File(BaresipService.filesPath + "/ca_certs.crt")
- oldCaFile = caCertsFile.exists()
-
- oldUserAgent = Config.variable("user_agent")
- newUserAgent = oldUserAgent
-
- powerManager = getSystemService(POWER_SERVICE) as PowerManager
- oldBatteryOptimizations = powerManager
- .isIgnoringBatteryOptimizations(packageName) == false
- newBatteryOptimizations = oldBatteryOptimizations
-
- androidSettingsRequest = registerForActivityResult(
- ActivityResultContracts.StartActivityForResult()
- ) {
- newBatteryOptimizations = powerManager
- .isIgnoringBatteryOptimizations(packageName) == false
- }
-
- dialerRoleRequest = registerForActivityResult(
- ActivityResultContracts.StartActivityForResult()
- ) {
- Log.d(TAG, "dialerRoleRequest succeeded: " +
- "${it.resultCode == RESULT_OK}")
- if (VERSION.SDK_INT >= 29)
- newDefaultDialer = roleManager.isRoleHeld(RoleManager.ROLE_DIALER)
- }
-
- if (VERSION.SDK_INT >= 29) {
- roleManager = getSystemService(ROLE_SERVICE) as RoleManager
- oldDefaultDialer = roleManager.isRoleHeld(RoleManager.ROLE_DIALER)
- }
-
- oldContactsMode = Config.variable("contacts_mode").lowercase()
- newContactsMode = oldContactsMode
-
- oldDarkTheme = Preferences(applicationContext).displayTheme ==
- AppCompatDelegate.MODE_NIGHT_YES
- newDarkTheme = oldDarkTheme
-
- oldDebug = Config.variable("log_level") == "0"
- newDebug = oldDebug
-
- oldSipTrace = BaresipService.sipTrace
- newSipTrace = oldSipTrace
-
- requestPermissionsLauncher =
- registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { }
-
- setContent {
- AppTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = LocalCustomColors.current.background
- ) {
- ConfigScreen(this, title) { goBack() }
- }
- }
- }
- }
-
- @Composable
- fun ConfigScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
- Scaffold(
- modifier = Modifier
- .fillMaxHeight()
- .imePadding()
- .safeDrawingPadding(),
- containerColor = LocalCustomColors.current.background,
- topBar = { TopAppBar(title, navigateBack) },
- content = { contentPadding ->
- ConfigContent(ctx, contentPadding)
- }
- )
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- fun TopAppBar(title: String, navigateBack: () -> Unit) {
- TopAppBar(
- title = {
- Text(
- text = title,
- color = LocalCustomColors.current.light,
- fontWeight = FontWeight.Bold
- )
- },
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = LocalCustomColors.current.primary
- ),
- navigationIcon = {
- IconButton(onClick = navigateBack) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = LocalCustomColors.current.light
- )
- }
- },
- actions = {
- IconButton(onClick = {
- checkOnClick()
- }) {
- Icon(
- imageVector = Icons.Filled.Check,
- tint = LocalCustomColors.current.light,
- contentDescription = "Check"
- )
- }
- }
- )
- }
-
- @Composable
- fun ConfigContent(ctx: Context, contentPadding: PaddingValues) {
-
- if (showAlert.value) {
- AlertDialog(
- showDialog = showAlert,
- title = alertTitle.value,
- message = alertMessage.value,
- positiveButtonText = stringResource(R.string.ok),
- )
- }
-
-
- if (showDialog.value)
- AlertDialog(
- showDialog = showDialog,
- title = dialogTitle.value,
- message = dialogMessage.value,
- positiveButtonText = positiveText.value,
- onPositiveClicked = onPositiveClicked.value,
- negativeButtonText = negativeText.value,
- onNegativeClicked = onNegativeClicked.value,
- )
-
- val scrollState = rememberScrollState()
-
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(contentPadding)
- .padding(start = 16.dp, end = 4.dp, top = 16.dp, bottom = 8.dp)
- .verticalScrollbar(scrollState)
- .verticalScroll(state = scrollState),
- verticalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- StartAutomatically(ctx)
- ListenAddress()
- AddressFamily()
- DnsServers()
- TlsCertificateFile(ctx)
- VerifyServer()
- CaFile(ctx)
- UserAgent()
- AudioSettings(ctx)
- Ringtone()
- BatteryOptimizations()
- if (VERSION.SDK_INT >= 29)
- DefaultDialer()
- Contacts(ctx)
- DarkTheme()
- Debug()
- SipTrace()
- Reset()
- }
- }
-
- @Composable
- private fun StartAutomatically(ctx: Context) {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.start_automatically),
- modifier = Modifier.weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.start_automatically)
- alertMessage.value = getString(R.string.start_automatically_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- var startAutomatically by remember { mutableStateOf(oldAutoStart) }
- Switch(
- checked = startAutomatically,
- onCheckedChange = {
- if (it) {
- if (!isAppearOnTopPermissionGranted(ctx)) {
- dialogTitle.value = getString(R.string.notice)
- dialogMessage.value = getString(R.string.appear_on_top_permission)
- positiveText.value = getString(R.string.ok)
- onPositiveClicked.value = {
- val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
- startActivity(intent)
- }
- negativeText.value = getString(R.string.cancel)
- onNegativeClicked.value = {
- negativeText.value = ""
- }
- showDialog.value = true
- startAutomatically = false
- }
- else
- startAutomatically = true
- }
- else
- startAutomatically = false
- newAutoStart = startAutomatically
- }
- )
- }
- }
-
- @Composable
- private fun ListenAddress() {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start,
- ) {
- var listenAddr by remember { mutableStateOf(oldListenAddr) }
- newListenAddr = listenAddr
- OutlinedTextField(
- value = listenAddr,
- placeholder = { Text(stringResource(R.string._0_0_0_0_5060)) },
- onValueChange = {
- listenAddr = it
- newListenAddr = listenAddr
- },
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.listen_address)
- alertMessage.value = getString(R.string.listen_address_help)
- showAlert.value = true
- },
- textStyle = androidx.compose.ui.text.TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.listen_address)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- private fun AddressFamily() {
- Row(
- Modifier.fillMaxWidth().padding(top = 12.dp).padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.address_family),
- modifier = Modifier
- .weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.address_family)
- alertMessage.value = getString(R.string.address_family_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- val isDropDownExpanded = remember {
- mutableStateOf(false)
- }
- val familyNames = listOf("--", "IPv4", "IPv6")
- val familyValues = listOf("", "ipv4", "ipv6")
- val itemPosition = remember {
- mutableIntStateOf(familyValues.indexOf(oldAddressFamily))
- }
- Box {
- Row(
- horizontalArrangement = Arrangement.End,
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.clickable {
- isDropDownExpanded.value = true
- }
- ) {
- Text(text = familyNames[itemPosition.intValue],
- color = LocalCustomColors.current.itemText)
- CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
- tint = LocalCustomColors.current.itemText)
- }
- DropdownMenu(
- expanded = isDropDownExpanded.value,
- onDismissRequest = {
- isDropDownExpanded.value = false
- }) {
- familyNames.forEachIndexed { index, family ->
- DropdownMenuItem(text = {
- Text(text = family)
- },
- onClick = {
- isDropDownExpanded.value = false
- itemPosition.intValue = index
- newAddressFamily = familyValues[index]
- })
- if (index < 2)
- HorizontalDivider(
- thickness = 1.dp,
- color = LocalCustomColors.current.itemText
- )
- }
- }
- }
- }
- }
-
- @Composable
- private fun DnsServers() {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var dnsServers by remember { mutableStateOf(oldDnsServers) }
- newDnsServers = dnsServers
- OutlinedTextField(
- value = dnsServers,
- onValueChange = {
- dnsServers = it
- newDnsServers = dnsServers
- },
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.dns_servers)
- alertMessage.value = getString(R.string.dns_servers_help)
- showAlert.value = true
- },
- textStyle = androidx.compose.ui.text.TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText
- ),
- label = { LabelText(stringResource(R.string.dns_servers)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- private fun TlsCertificateFile(ctx: Context) {
- val showAlertDialog = remember { mutableStateOf(false) }
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.tls_certificate_file),
- modifier = Modifier
- .weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.tls_certificate_file)
- alertMessage.value = getString(R.string.tls_certificate_file_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- var tlsCertificateFile by remember { mutableStateOf(oldTlsCertificateFile) }
- Switch(
- checked = tlsCertificateFile,
- onCheckedChange = {
- tlsCertificateFile = it
- newTlsCertificateFile = tlsCertificateFile
- if (it)
- if (VERSION.SDK_INT < 29) {
- tlsCertificateFile = false
- when {
- ContextCompat.checkSelfPermission(
- ctx,
- Manifest.permission.READ_EXTERNAL_STORAGE
- ) == PackageManager.PERMISSION_GRANTED -> {
- Log.d(TAG, "Read External Storage permission granted")
- val downloadsPath = Utils.downloadsPath("cert.pem")
- val content = Utils.getFileContents(downloadsPath)
- if (content == null) {
- alertTitle.value = getString(R.string.error)
- alertMessage.value = getString(R.string.read_cert_error)
- showAlert.value = true
- return@Switch
- }
- val certPath = BaresipService.filesPath + "/cert.pem"
- Utils.putFileContents(certPath, content)
- Config.replaceVariable("sip_certificate", certPath)
- tlsCertificateFile = true
- save = true
- restart = true
- }
- shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE) -> {
- showAlertDialog.value = true
- }
- else ->
- requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
- }
- }
- else
- Utils.selectInputFile(certificateRequest)
- else {
- Config.removeVariable("sip_certificate")
- Utils.deleteFile(File(BaresipService.filesPath + "/cert.pem"))
- save = true
- restart = true
- }
- }
- )
- }
- if (showAlertDialog.value)
- AlertDialog(
- showDialog = showAlertDialog,
- title = stringResource(R.string.notice),
- message = stringResource(R.string.no_read_permission),
- positiveButtonText = stringResource(R.string.ok),
- onPositiveClicked = { requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) },
- negativeButtonText = "",
- onNegativeClicked = {},
- )
- }
-
- @Composable
- private fun VerifyServer() {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.verify_server),
- modifier = Modifier
- .weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.verify_server)
- alertMessage.value = getString(R.string.verify_server_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- var verifyServer by remember { mutableStateOf(oldVerifyServer) }
- Switch(
- checked = verifyServer,
- onCheckedChange = {
- verifyServer = it
- newVerifyServer = verifyServer
- }
- )
- }
- }
-
- @Composable
- private fun CaFile(ctx: Context) {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.tls_ca_file),
- modifier = Modifier
- .weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.tls_ca_file)
- alertMessage.value = getString(R.string.tls_ca_file_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- var caFile by remember { mutableStateOf(oldCaFile) }
- Switch(
- checked = caFile,
- onCheckedChange = {
- caFile = it
- newCaFile = caFile
- if (it) {
- if (VERSION.SDK_INT < 29) {
- caFile = false
- when {
- ContextCompat.checkSelfPermission(ctx,
- Manifest.permission.READ_EXTERNAL_STORAGE
- ) == PackageManager.PERMISSION_GRANTED -> {
- Log.d(TAG, "Read External Storage permission granted")
- val downloadsPath = Utils.downloadsPath("ca_certs.crt")
- val content = Utils.getFileContents(downloadsPath)
- if (content == null) {
- alertTitle.value = getString(R.string.error)
- alertMessage.value = getString(R.string.read_ca_certs_error)
- showAlert.value = true
- return@Switch
- }
- File(BaresipService.filesPath + "/ca_certs.crt").writeBytes(content)
- caFile = true
- restart = true
- }
- shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE) -> {
- dialogTitle.value = getString(R.string.notice)
- dialogMessage.value = getString(R.string.no_read_permission)
- positiveText.value = getString(R.string.ok)
- onPositiveClicked.value = {
- requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
- }
- negativeText.value = ""
- showDialog.value = true
- }
- else ->
- requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
- }
- }
- else
- Utils.selectInputFile(caCertsRequest)
- }
- else {
- Utils.deleteFile(File(BaresipService.filesPath + "/ca_certs.crt"))
- restart = true
- }
- }
- )
- }
- }
-
- @Composable
- private fun UserAgent() {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- var userAgent by remember { mutableStateOf(oldUserAgent) }
- newUserAgent = userAgent
- OutlinedTextField(
- value = userAgent,
- placeholder = { Text(stringResource(R.string.user_agent)) },
- onValueChange = {
- userAgent = it
- newUserAgent = userAgent
- },
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- alertTitle.value = getString(R.string.user_agent)
- alertMessage.value = getString(R.string.user_agent_help)
- showAlert.value = true
- },
- textStyle = androidx.compose.ui.text.TextStyle(
- fontSize = 18.sp, color = LocalCustomColors.current.itemText),
- label = { LabelText(stringResource(R.string.user_agent)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- }
- }
-
- @Composable
- private fun AudioSettings(ctx: Context) {
- Row(
- Modifier.fillMaxWidth().padding(top = 12.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(
- text = stringResource(R.string.audio_settings),
- modifier = Modifier
- .weight(1f)
- .clickable {
- audioRequest.launch(Intent(ctx, AudioActivity::class.java))
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp,
- fontWeight = FontWeight. Bold
- )
- }
- }
-
- @Composable
- private fun Ringtone() {
- oldRingtoneUri = if (Preferences(applicationContext).ringtoneUri == "")
- RingtoneManager.getActualDefaultRingtoneUri(applicationContext, RingtoneManager.TYPE_RINGTONE).toString()
- else
- Preferences(applicationContext).ringtoneUri!!
- newRingtoneUri = oldRingtoneUri
- val launcher = rememberLauncherForActivityResult(
- ActivityResultContracts.StartActivityForResult()
- ) { result: ActivityResult ->
- if (result.resultCode == RESULT_OK) {
- val uri: Uri? = if (VERSION.SDK_INT >= 33)
- result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Uri::class.java)
- else
- @Suppress("DEPRECATION")
- result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
- if (uri != null)
- newRingtoneUri = uri.toString()
- }
- }
- Row(
- Modifier
- .fillMaxWidth()
- .padding(top = 12.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(
- text = stringResource(R.string.ringtone),
- modifier = Modifier
- .weight(1f)
- .clickable {
- val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER)
- intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE,
- RingtoneManager.TYPE_RINGTONE)
- intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, getString(R.string.select_ringtone))
- intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, newRingtoneUri.toUri())
- intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false)
- intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
- launcher.launch(intent)
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp,
- fontWeight = FontWeight.Bold
- )
- }
- }
-
- @Composable
- private fun BatteryOptimizations() {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.battery_optimizations),
- modifier = Modifier
- .weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.battery_optimizations)
- alertMessage.value = getString(R.string.battery_optimizations_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- var battery by remember { mutableStateOf(oldBatteryOptimizations) }
- Switch(
- checked = battery,
- onCheckedChange = {
- battery = it
- newBatteryOptimizations = battery
- try {
- androidSettingsRequest.launch(Intent("android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS"))
- } catch (e: ActivityNotFoundException) {
- Log.e(TAG, "ActivityNotFound exception: ${e.message}")
- }
- }
- )
- }
- }
-
- @RequiresApi(29)
- @Composable
- private fun DefaultDialer() {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.default_phone_app),
- modifier = Modifier
- .weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.default_phone_app)
- alertMessage.value = getString(R.string.default_phone_app_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- var defaultDialer by remember { mutableStateOf(oldDefaultDialer) }
- Switch(
- checked = defaultDialer,
- onCheckedChange = {
- defaultDialer = it
- newDefaultDialer = defaultDialer
- if (it) {
- if (!roleManager.isRoleAvailable(RoleManager.ROLE_DIALER)) {
- alertTitle.value = getString(R.string.alert)
- alertMessage.value = getString(R.string.dialer_role_not_available)
- showAlert.value = true
- }
- else
- if (!roleManager.isRoleHeld(RoleManager.ROLE_DIALER))
- dialerRoleRequest.launch(roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER))
- } else {
- try {
- dialerRoleRequest.launch(Intent("android.settings.MANAGE_DEFAULT_APPS_SETTINGS"))
- } catch (e: ActivityNotFoundException) {
- Log.e(TAG, "ActivityNotFound exception: ${e.message}")
- }
- }
- }
- )
- }
- }
-
- @Composable
- private fun Contacts(ctx: Context) {
- val showAlertDialog = remember { mutableStateOf(false) }
- Row(
- Modifier.fillMaxWidth().padding(top = 12.dp).padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.contacts),
- modifier = Modifier
- .weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.contacts)
- alertMessage.value = getString(R.string.contacts_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- val isDropDownExpanded = remember {
- mutableStateOf(false)
- }
- val contactNames = listOf("baresip", "Android", "Both")
- val contactValues = listOf("baresip", "android", "both")
- val itemPosition = remember {
- mutableIntStateOf(contactValues.indexOf(oldContactsMode))
- }
- val contactsPermissions = arrayOf(Manifest.permission.READ_CONTACTS,
- Manifest.permission.WRITE_CONTACTS)
- Box {
- Row(
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.clickable {
- isDropDownExpanded.value = true
- }
- ) {
- Text(text = contactNames[itemPosition.intValue],
- color = LocalCustomColors.current.itemText)
- CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
- tint = LocalCustomColors.current.itemText)
- }
- DropdownMenu(
- expanded = isDropDownExpanded.value,
- onDismissRequest = {
- isDropDownExpanded.value = false
- }) {
- contactNames.forEachIndexed { index, name ->
- DropdownMenuItem(text = {
- Text(text = name)
- },
- onClick = {
- isDropDownExpanded.value = false
- val mode = contactValues[index]
- if (mode != "baresip" && !Utils.checkPermissions(applicationContext, contactsPermissions)) {
- dialogTitle.value = getString(R.string.consent_request)
- dialogMessage.value = getString(R.string.contacts_consent)
- positiveText.value = getString(R.string.accept)
- onPositiveClicked.value = {
- showDialog.value = false
- newContactsMode = mode
- if (ContextCompat.checkSelfPermission(
- ctx,
- Manifest.permission.READ_CONTACTS
- ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(
- ctx,
- Manifest.permission.WRITE_CONTACTS
- ) == PackageManager.PERMISSION_GRANTED
- ) {
- Log.d(TAG, "Contacts permissions already granted")
- } else {
- if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS) ||
- shouldShowRequestPermissionRationale(Manifest.permission.WRITE_CONTACTS))
- showAlertDialog.value = true
- else
- requestPermissionsLauncher.launch(
- arrayOf(
- Manifest.permission.READ_CONTACTS,
- Manifest.permission.WRITE_CONTACTS
- )
- )
- }
- }
- negativeText.value = getString(R.string.deny)
- onNegativeClicked.value = {
- itemPosition.intValue = contactValues.indexOf(oldContactsMode)
- negativeText.value = ""
- }
- showDialog.value = true
- }
- else {
- itemPosition.intValue = index
- newContactsMode = contactValues[index]
- }
- })
- if (index < 2)
- HorizontalDivider(
- thickness = 1.dp,
- color = LocalCustomColors.current.itemText
- )
- }
- }
- }
- if (showAlertDialog.value)
- AlertDialog(
- showDialog = showAlertDialog,
- title = stringResource(R.string.notice),
- message = stringResource(R.string.no_android_contacts),
- positiveButtonText = stringResource(R.string.ok),
- onPositiveClicked = { requestPermissionsLauncher.launch(
- arrayOf(
- Manifest.permission.READ_CONTACTS,
- Manifest.permission.WRITE_CONTACTS
- )
- )},
- negativeButtonText = "",
- onNegativeClicked = {},
- )
- }
- }
-
- @Composable
- private fun DarkTheme() {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.dark_theme),
- modifier = Modifier
- .weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.dark_theme)
- alertMessage.value = getString(R.string.dark_theme_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- var darkTheme by remember { mutableStateOf(oldDarkTheme) }
- Switch(
- checked = darkTheme,
- onCheckedChange = {
- darkTheme = it
- newDarkTheme = darkTheme
- }
- )
- }
- }
-
- @Composable
- private fun Debug() {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.debug),
- modifier = Modifier
- .weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.debug)
- alertMessage.value = getString(R.string.debug_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- var debug by remember { mutableStateOf(oldDebug) }
- Switch(
- checked = debug,
- onCheckedChange = {
- debug = it
- newDebug = debug
- }
- )
- }
- }
-
- @Composable
- private fun SipTrace() {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.sip_trace),
- modifier = Modifier
- .weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.sip_trace)
- alertMessage.value = getString(R.string.sip_trace_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- var sipTrace by remember { mutableStateOf(oldSipTrace) }
- Switch(
- checked = sipTrace,
- onCheckedChange = {
- sipTrace = it
- newSipTrace = sipTrace
- }
- )
- }
- }
-
- @Composable
- private fun Reset() {
- Row(
- Modifier.fillMaxWidth().padding(end = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Text(text = stringResource(R.string.reset_config),
- modifier = Modifier
- .weight(1f)
- .clickable {
- alertTitle.value = getString(R.string.reset_config)
- alertMessage.value = getString(R.string.reset_config_help)
- showAlert.value = true
- },
- color = LocalCustomColors.current.itemText,
- fontSize = 18.sp)
- var reset by remember { mutableStateOf(false) }
- Switch(
- checked = reset,
- onCheckedChange = {
- dialogTitle.value = getString(R.string.confirmation)
- dialogMessage.value = getString(R.string.reset_config_alert)
- positiveText.value = getString(R.string.reset)
- onPositiveClicked.value = {
- Config.reset()
- save = false
- restart = true
- done()
- }
- negativeText.value = getString(R.string.cancel)
- onNegativeClicked.value = {
- reset = false
- negativeText.value = ""
- }
- showDialog.value = true
- }
- )
- }
- }
-
- private fun checkOnClick() {
-
- if (BaresipService.activities.indexOf("config") == -1)
- return
-
- if (oldAutoStart != newAutoStart) {
- Config.replaceVariable("auto_start",
- if (newAutoStart) "yes" else "no")
- save = true
- }
-
- val listenAddr = newListenAddr.trim()
- if (listenAddr != oldListenAddr) {
- if ((listenAddr != "") && !Utils.checkIpPort(listenAddr)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = "${getString(R.string.invalid_listen_address)}: $listenAddr"
- showAlert.value = true
- return
- }
- Config.replaceVariable("sip_listen", listenAddr)
- save = true
- restart = true
- }
-
- if (oldAddressFamily != newAddressFamily) {
- Config.replaceVariable("net_af", newAddressFamily)
- save = true
- restart = true
- }
-
- var dnsServers = newDnsServers.lowercase(Locale.ROOT)
- .replace(" ", "")
- dnsServers = addMissingPorts(dnsServers)
- if (dnsServers != oldDnsServers.replace(" ", "")) {
- if (!checkDnsServers(dnsServers)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = "${getString(R.string.invalid_dns_servers)}: $dnsServers"
- showAlert.value = true
- return
- }
- Config.removeVariable("dns_server")
- if (dnsServers.isNotEmpty()) {
- for (server in dnsServers.split(","))
- Config.addVariable("dns_server", server)
- Config.replaceVariable("dyn_dns", "no")
- if (Api.net_use_nameserver(dnsServers) != 0) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = "${getString(R.string.failed_to_set_dns_servers)}: $dnsServers"
- showAlert.value = true
- return
- }
- } else {
- Config.replaceVariable("dyn_dns", "yes")
- Config.updateDnsServers(BaresipService.dnsServers)
- }
- // Api.net_dns_debug()
- save = true
- }
-
- if (oldVerifyServer != newVerifyServer) {
- Config.replaceVariable("sip_verify_server", if (newVerifyServer) "yes" else "no")
- Api.config_verify_server_set(newVerifyServer)
- save = true
- }
-
- newUserAgent = newUserAgent.trim()
- if (newUserAgent != oldUserAgent) {
- if ((newUserAgent != "") && !Utils.checkServerVal(newUserAgent)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = "${getString(R.string.invalid_user_agent)}: $newUserAgent"
- showAlert.value = true
- return
- }
- if (newUserAgent != "")
- Config.replaceVariable("user_agent", newUserAgent)
- else
- Config.removeVariable("user_agent")
- save = true
- restart = true
- }
-
- Log.d(TAG, "Old/new ringtone: $oldRingtoneUri / $newRingtoneUri")
- if (newRingtoneUri != oldRingtoneUri) {
- Preferences(applicationContext).ringtoneUri = newRingtoneUri
- BaresipService.rt = RingtoneManager.getRingtone(applicationContext, newRingtoneUri.toUri())
- }
- if (oldContactsMode != newContactsMode) {
- Config.replaceVariable("contacts_mode", newContactsMode)
- BaresipService.contactsMode = newContactsMode
- when (newContactsMode) {
- "baresip" -> {
- BaresipService.androidContacts.value = listOf()
- Contact.restoreBaresipContacts()
- baresipService.action = "Stop Content Observer"
- }
- "android" -> {
- BaresipService.baresipContacts.value = mutableListOf()
- Contact.loadAndroidContacts(this)
- baresipService.action = "Start Content Observer"
- }
- "both" -> {
- Contact.restoreBaresipContacts()
- Contact.loadAndroidContacts(this)
- baresipService.action = "Start Content Observer"
- }
- }
- Contact.contactsUpdate()
- startService(baresipService)
- save = true
- }
-
- val newDisplayTheme = if (newDarkTheme)
- AppCompatDelegate.MODE_NIGHT_YES
- else
- AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
- if (oldDarkTheme != newDarkTheme) {
- Preferences(applicationContext).displayTheme = newDisplayTheme
- BaresipService.darkTheme.value = newDarkTheme
- Config.replaceVariable("dark_theme",
- if (newDarkTheme) "yes" else "no")
- save = true
- }
-
- if (oldDebug != newDebug) {
- val logLevelString = if (newDebug) "0" else "2"
- Config.replaceVariable("log_level", logLevelString)
- Api.log_level_set(logLevelString.toInt())
- Log.logLevelSet(logLevelString.toInt())
- save = true
- }
-
- if (oldSipTrace != newSipTrace) {
- BaresipService.sipTrace = newSipTrace
- Api.uag_enable_sip_trace(newSipTrace)
- }
-
- done()
-
- }
-
- override fun onStart() {
- super.onStart()
- requestPermissionLauncher =
- registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
- }
-
- override fun onDestroy() {
- if (VERSION.SDK_INT >= 33) {
- if (backInvokedCallback != null)
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
- }
- else
- onBackPressedCallback.remove()
- super.onDestroy()
- }
-
- private fun isAppearOnTopPermissionGranted(ctx: Context): Boolean {
- return Settings.canDrawOverlays(ctx)
- }
-
- private fun done() {
- if (save)
- Config.save()
- BaresipService.activities.remove("config")
- val intent = Intent(this, MainActivity::class.java)
- if (restart || audioRestart)
- intent.putExtra("restart", true)
- setResult(RESULT_OK, intent)
- finish()
- }
-
- private fun goBack() {
- BaresipService.activities.remove("config")
- if (audioRestart) {
- val intent = Intent(this, MainActivity::class.java)
- intent.putExtra("restart", true)
- setResult(RESULT_OK, intent)
- } else {
- setResult(RESULT_CANCELED, Intent(this, MainActivity::class.java))
- }
- finish()
- }
-
- private fun checkDnsServers(dnsServers: String): Boolean {
- if (dnsServers.isEmpty()) return true
- for (server in dnsServers.split(","))
- if (!Utils.checkIpPort(server.trim())) return false
- return true
- }
-
- private fun addMissingPorts(addressList: String): String {
- if (addressList == "") return ""
- var result = ""
- for (addr in addressList.split(","))
- result = if (Utils.checkIpPort(addr)) {
- "$result,$addr"
- } else {
- if (Utils.checkIpV4(addr))
- "$result,$addr:53"
- else
- "$result,[$addr]:53"
- }
- return result.substring(1)
- }
-
-}
-
diff --git a/app/src/main/kotlin/com/tutpro/baresip/Constants.kt b/app/src/main/kotlin/com/tutpro/baresip/Constants.kt
index 614c6c85..5e5e96dc 100644
--- a/app/src/main/kotlin/com/tutpro/baresip/Constants.kt
+++ b/app/src/main/kotlin/com/tutpro/baresip/Constants.kt
@@ -23,6 +23,7 @@ const val MESSAGE_REQ_CODE = 8
const val REPLY_REQ_CODE = 9
const val SAVE_REQ_CODE = 10
const val DELETE_REQ_CODE = 11
+const val DIRECT_REPLY_REQ_CODE = 12
const val REGISTRATION_INTERVAL = 900
const val NO_AUTH_PASS = "t%Qa?~?J8,~6"
@@ -32,6 +33,10 @@ const val MESSAGE_UP = 2131165306
const val MESSAGE_UP_FAIL = 2131165307
const val MESSAGE_UP_WAIT = 2131165308
+val mediaEncMap = mapOf("zrtp" to "ZRTP", "dtls_srtp" to "DTLS-SRTPF", "srtp-mand" to "SRTP-MAND",
+ "srtp" to "SRTP", "" to "--")
+
+val mediaNatMap = mapOf("stun" to "STUN", "turn" to "TURN", "ice" to "ICE", "" to "--")
diff --git a/app/src/main/kotlin/com/tutpro/baresip/ContactsActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/ContactsActivity.kt
deleted file mode 100644
index 7e87dfa7..00000000
--- a/app/src/main/kotlin/com/tutpro/baresip/ContactsActivity.kt
+++ /dev/null
@@ -1,458 +0,0 @@
-package com.tutpro.baresip
-
-import android.app.Activity
-import android.content.ContentValues
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import android.os.Bundle
-import android.os.SystemClock
-import android.provider.ContactsContract
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Edit
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SmallFloatingActionButton
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.lifecycle.Observer
-import coil.compose.AsyncImage
-import com.tutpro.baresip.CustomElements.AlertDialog
-import com.tutpro.baresip.CustomElements.TextAvatar
-import com.tutpro.baresip.CustomElements.verticalScrollbar
-import java.io.File
-import java.io.IOException
-
-class ContactsActivity : ComponentActivity() {
-
- private lateinit var aor: String
- private var newAndroidName: String? = null
- private var lastClick: Long = 0
-
- private var backInvokedCallback: OnBackInvokedCallback? = null
- private lateinit var onBackPressedCallback: OnBackPressedCallback
-
- @RequiresApi(33)
- private fun registerBackInvokedCallback() {
- backInvokedCallback = OnBackInvokedCallback { goBack() }
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- backInvokedCallback!!
- )
- }
-
- private val contactRequest =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == RESULT_OK) {
- if (it.data != null && it.data!!.hasExtra("name"))
- newAndroidName = it.data!!.getStringExtra("name")
- }
- }
-
- public override fun onCreate(savedInstanceState: Bundle?) {
-
- super.onCreate(savedInstanceState)
-
- enableEdgeToEdge()
-
- if (Build.VERSION.SDK_INT >= 33)
- registerBackInvokedCallback()
- else {
- onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- goBack()
- }
- }
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
- val title = getString(R.string.contacts)
-
- aor = intent.getStringExtra("aor")!!
- Utils.addActivity("contacts,$aor")
-
- val androidContactsObserver = Observer {
- if (newAndroidName != null) {
- val contentValues = ContentValues()
- contentValues.put(ContactsContract.Contacts.STARRED, 1)
- try {
- this.contentResolver.update(
- ContactsContract.RawContacts.CONTENT_URI, contentValues,
- ContactsContract.Contacts.DISPLAY_NAME + "='" + newAndroidName + "'", null
- )
- } catch (e: Exception) {
- Log.e(TAG, "Update of Android favorite failed: ${e.message}")
- }
- newAndroidName = null
- }
- //Contact.contactsUpdate()
- }
- BaresipService.contactUpdate.observe(this, androidContactsObserver)
-
- setContent {
- AppTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = LocalCustomColors.current.background
- ) {
- ContactsScreen(LocalContext.current, title) {
- goBack()
- }
- }
- }
- }
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- fun ContactsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
- Scaffold(
- modifier = Modifier
- .fillMaxHeight()
- .imePadding()
- .safeDrawingPadding(),
- containerColor = LocalCustomColors.current.background,
- topBar = {
- TopAppBar(
- title = {
- Text(
- text = title,
- color = LocalCustomColors.current.light,
- fontWeight = FontWeight.Bold
- )
- },
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = LocalCustomColors.current.primary
- ),
- navigationIcon = {
- IconButton(onClick = navigateBack) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = LocalCustomColors.current.light
- )
- }
- },
- )
- },
- floatingActionButton = {
- SmallFloatingActionButton(onClick = {
- if (SystemClock.elapsedRealtime() - lastClick > 1000) {
- lastClick = SystemClock.elapsedRealtime()
- val intent = Intent(ctx, BaresipContactActivity::class.java)
- val b = Bundle()
- b.putBoolean("new", true)
- b.putString("uri", "")
- intent.putExtras(b)
- contactRequest.launch(intent)
- }},
- containerColor = LocalCustomColors.current.accent,
- contentColor = LocalCustomColors.current.background
- ) {
- Icon(imageVector = Icons.Filled.Add,
- modifier = Modifier.size(36.dp),
- contentDescription = stringResource(R.string.add)
- )
- }
- },
- content = { contentPadding ->
- ContactsContent(ctx, contentPadding)
- }
- )
- }
-
- @OptIn(ExperimentalFoundationApi::class)
- @Composable
- fun ContactsContent(ctx: Context, contentPadding: PaddingValues) {
-
- val showDialog = remember { mutableStateOf(false) }
- val dialogMessage = remember { mutableStateOf("") }
- val positiveText = remember { mutableStateOf("") }
- val positiveAction = remember { mutableStateOf({}) }
- val neutralText = remember { mutableStateOf("") }
- val neutralAction = remember { mutableStateOf({}) }
-
- if (showDialog.value)
- AlertDialog(
- showDialog = showDialog,
- title = stringResource(R.string.confirmation),
- message = dialogMessage.value,
- positiveButtonText = positiveText.value,
- onPositiveClicked = positiveAction.value,
- neutralButtonText = neutralText.value,
- onNeutralClicked = neutralAction.value,
- negativeButtonText = stringResource(R.string.cancel)
- )
-
- val lazyListState = rememberLazyListState()
-
- LazyColumn(
- modifier = Modifier
- .fillMaxWidth()
- .background(LocalCustomColors.current.background)
- .padding(contentPadding)
- .padding(start = 16.dp, end = 4.dp, top = 16.dp, bottom = 64.dp)
- .verticalScrollbar(
- state = lazyListState,
- width = 4.dp,
- color = LocalCustomColors.current.gray
- ),
- state = lazyListState,
- verticalArrangement = Arrangement.spacedBy(10.dp),
- ) {
- items(BaresipService.contacts, key = { it.id() }) { contact ->
-
- val name = contact.name()
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- when(contact) {
-
- is Contact.BaresipContact -> {
- val avatarImage = contact.avatarImage
- if (avatarImage != null)
- Image(
- bitmap = avatarImage.asImageBitmap(),
- contentDescription = "Avatar",
- contentScale = ContentScale.Crop,
- modifier = Modifier
- .size(36.dp)
- .clip(CircleShape)
- )
- else
- TextAvatar(name, contact.color)
- }
-
- is Contact.AndroidContact -> {
- val thumbNailUri = contact.thumbnailUri
- if (thumbNailUri != null)
- AsyncImage(
- model = thumbNailUri,
- contentDescription = "Avatar",
- contentScale = ContentScale.Crop,
- modifier = Modifier
- .size(36.dp)
- .clip(CircleShape),
- )
- else
- TextAvatar(name, contact.color)
- }
- }
-
- when(contact) {
-
- is Contact.BaresipContact -> {
- Text(text = name,
- fontSize = 20.sp,
- fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal,
- color = LocalCustomColors.current.itemText,
- modifier = Modifier
- .weight(1f)
- .padding(start = 10.dp)
- .combinedClickable(
- onClick = {
- dialogMessage.value = String.format(
- getString(R.string.contact_action_question),
- name
- )
- positiveText.value = getString(R.string.call)
- positiveAction.value = {
- val i = Intent(ctx, MainActivity::class.java)
- i.flags =
- Intent.FLAG_ACTIVITY_NEW_TASK or
- Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
- i.putExtra("action", "call")
- val ua = UserAgent.ofAor(aor)
- if (ua == null)
- Log.w(TAG, "onClickListener did not find AoR $aor")
- else {
- BaresipService.activities.clear()
- i.putExtra("uap", ua.uap)
- i.putExtra("peer", contact.uri)
- (ctx as Activity).startActivity(i)
- }
- }
- neutralText.value = getString(R.string.send_message)
- neutralAction.value = {
- val i = Intent(
- ctx,
- MainActivity::class.java
- )
- i.flags =
- Intent.FLAG_ACTIVITY_NEW_TASK or
- Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
- i.putExtra("action", "message")
- val ua = UserAgent.ofAor(aor)
- if (ua == null)
- Log.w(TAG, "onClickListener did not find AoR $aor")
- else {
- BaresipService.activities.clear()
- i.putExtra("uap", ua.uap)
- i.putExtra("peer", contact.uri)
- (ctx as Activity).startActivity(i)
- }
- }
- showDialog.value = true
- },
- onLongClick = {
- dialogMessage.value = String.format(
- getString(R.string.contact_delete_question),
- name
- )
- positiveText.value = getString(R.string.delete)
- positiveAction.value = {
- val id = contact.id
- val avatarFile = File(
- BaresipService.filesPath,
- "$id.png"
- )
- if (avatarFile.exists()) {
- try {
- avatarFile.delete()
- } catch (e: IOException) {
- Log.e(
- TAG,
- "Could not delete file $id.png: ${e.message}"
- )
- }
- }
- Contact.removeBaresipContact(contact)
- }
- showDialog.value = true
- }
- )
- )
- SmallFloatingActionButton(
- modifier = Modifier.padding(end = 10.dp),
- onClick = {
- if (SystemClock.elapsedRealtime() - lastClick > 1000) {
- lastClick = SystemClock.elapsedRealtime()
- val intent = Intent(ctx, BaresipContactActivity::class.java)
- val b = Bundle()
- b.putBoolean("new", false)
- b.putString("name", name)
- intent.putExtras(b)
- contactRequest.launch(intent)
- }
- },
- containerColor = LocalCustomColors.current.background,
- contentColor = LocalCustomColors.current.secondary
- ) {
- Icon(
- imageVector = Icons.Filled.Edit,
- modifier = Modifier.size(28.dp),
- contentDescription = stringResource(R.string.edit)
- )
- }
- }
-
- is Contact.AndroidContact -> {
- Text(text = name,
- fontSize = 20.sp,
- fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal,
- color = LocalCustomColors.current.itemText,
- modifier = Modifier
- .weight(1f)
- .padding(start = 10.dp, top = 4.dp, bottom = 4.dp)
- .combinedClickable(
- onClick = {
- if (SystemClock.elapsedRealtime() - lastClick > 1000) {
- lastClick = SystemClock.elapsedRealtime()
- val i =
- Intent(ctx, AndroidContactActivity::class.java)
- val b = Bundle()
- b.putString("aor", aor)
- b.putString("name", name)
- i.putExtras(b)
- ctx.startActivity(i, null)
- }
- },
- onLongClick = {
- dialogMessage.value = String.format(
- getString(R.string.contact_delete_question),
- name
- )
- positiveText.value = getString(R.string.delete)
- positiveAction.value = {
- ctx.contentResolver.delete(
- ContactsContract.RawContacts.CONTENT_URI,
- ContactsContract.Contacts.DISPLAY_NAME + "='" + name + "'",
- null
- )
- }
- showDialog.value = true
- }
- )
- )
- }
- }
- }
- }
- }
- }
-
- override fun onDestroy() {
- if (Build.VERSION.SDK_INT >= 33) {
- if (backInvokedCallback != null)
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
- }
- else
- onBackPressedCallback.remove()
- super.onDestroy()
- }
-
- private fun goBack() {
- BaresipService.activities.remove("contacts,$aor")
- setResult(RESULT_OK, Intent())
- finish()
- }
-
-}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/ContactsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/ContactsScreen.kt
new file mode 100644
index 00000000..15c4ef6f
--- /dev/null
+++ b/app/src/main/kotlin/com/tutpro/baresip/ContactsScreen.kt
@@ -0,0 +1,332 @@
+package com.tutpro.baresip
+
+import android.content.Context
+import android.content.Intent
+import android.provider.ContactsContract
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SmallFloatingActionButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import coil.compose.AsyncImage
+import com.tutpro.baresip.CustomElements.AlertDialog
+import com.tutpro.baresip.CustomElements.TextAvatar
+import com.tutpro.baresip.CustomElements.verticalScrollbar
+import java.io.File
+import java.io.IOException
+
+const val avatarSize: Int = 96
+
+fun NavGraphBuilder.contactsScreenRoute(
+ navController: NavController,
+ viewModel: ViewModel
+) {
+ composable("contacts") { backStackEntry ->
+ ContactsScreen(navController = navController, viewModel = viewModel)
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun ContactsScreen(navController: NavController, viewModel: ViewModel) {
+
+ val ctx = LocalContext.current
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize().imePadding(),
+ containerColor = LocalCustomColors.current.background,
+ topBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(
+ top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
+ )
+ ) {
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(R.string.contacts),
+ color = LocalCustomColors.current.light,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ colors = TopAppBarDefaults.mediumTopAppBarColors(
+ containerColor = LocalCustomColors.current.primary
+ ),
+ navigationIcon = {
+ IconButton(onClick = { navController.popBackStack() }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back",
+ tint = LocalCustomColors.current.light
+ )
+ }
+ },
+ windowInsets = WindowInsets(0, 0, 0, 0),
+ )
+ }
+ },
+ floatingActionButton = {
+ SmallFloatingActionButton(onClick = { navController.navigate("baresip_contact//new") },
+ containerColor = LocalCustomColors.current.accent,
+ contentColor = LocalCustomColors.current.background
+ ) {
+ Icon(imageVector = Icons.Filled.Add,
+ modifier = Modifier.size(36.dp),
+ contentDescription = stringResource(R.string.add)
+ )
+ }
+ },
+ content = { contentPadding ->
+ ContactsContent(ctx, viewModel, navController, contentPadding)
+ }
+ )
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun ContactsContent(
+ ctx: Context,
+ viewModel: ViewModel,
+ navController: NavController,
+ contentPadding: PaddingValues
+) {
+ val showDialog = remember { mutableStateOf(false) }
+ val dialogMessage = remember { mutableStateOf("") }
+ val positiveText = remember { mutableStateOf("") }
+ val positiveAction = remember { mutableStateOf({}) }
+ val neutralText = remember { mutableStateOf("") }
+ val neutralAction = remember { mutableStateOf({}) }
+
+ if (showDialog.value)
+ AlertDialog(
+ showDialog = showDialog,
+ title = stringResource(R.string.confirmation),
+ message = dialogMessage.value,
+ positiveButtonText = positiveText.value,
+ onPositiveClicked = positiveAction.value,
+ neutralButtonText = neutralText.value,
+ onNeutralClicked = neutralAction.value,
+ negativeButtonText = stringResource(R.string.cancel)
+ )
+
+ val lazyListState = rememberLazyListState()
+
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(contentPadding)
+ .padding(start = 16.dp, end = 4.dp, top = 16.dp, bottom = 64.dp)
+ .verticalScrollbar(
+ state = lazyListState,
+ width = 4.dp,
+ color = LocalCustomColors.current.gray
+ ),
+ state = lazyListState,
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ items(BaresipService.contacts, key = { it.id() }) { contact ->
+
+ val name = contact.name()
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ when(contact) {
+
+ is Contact.BaresipContact -> {
+ val avatarImage = contact.avatarImage
+ if (avatarImage != null)
+ Image(
+ bitmap = avatarImage.asImageBitmap(),
+ contentDescription = "Avatar",
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .size(36.dp)
+ .clip(CircleShape)
+ )
+ else
+ TextAvatar(name, contact.color)
+ }
+
+ is Contact.AndroidContact -> {
+ val thumbNailUri = contact.thumbnailUri
+ if (thumbNailUri != null)
+ AsyncImage(
+ model = thumbNailUri,
+ contentDescription = "Avatar",
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .size(36.dp)
+ .clip(CircleShape),
+ )
+ else
+ TextAvatar(name, contact.color)
+ }
+ }
+
+ when(contact) {
+
+ is Contact.BaresipContact -> {
+ Text(text = name,
+ fontSize = 20.sp,
+ fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal,
+ color = LocalCustomColors.current.itemText,
+ modifier = Modifier
+ .weight(1f)
+ .padding(start = 10.dp)
+ .combinedClickable(
+ onClick = {
+ val aor = viewModel.selectedAor.value
+ val ua = UserAgent.ofAor(aor)
+ val intent = Intent(ctx, MainActivity::class.java)
+ if (ua != null) {
+ intent.putExtra("uap", ua.uap)
+ intent.putExtra("peer", contact.uri)
+ }
+ else
+ Log.w(TAG, "onClickListener did not find UA for $aor")
+ dialogMessage.value = String.format(
+ ctx.getString(R.string.contact_action_question),
+ name
+ )
+ positiveText.value = ctx.getString(R.string.call)
+ positiveAction.value = {
+ if (ua != null) {
+ handleIntent(ctx, viewModel, intent, "call")
+ navController.popBackStack()
+ }
+ }
+ neutralText.value = ctx.getString(R.string.send_message)
+ neutralAction.value = {
+ if (ua != null) {
+ handleIntent(ctx, viewModel, intent, "message")
+ navController.popBackStack()
+ }
+ }
+ showDialog.value = true
+ },
+ onLongClick = {
+ dialogMessage.value = String.format(
+ ctx.getString(R.string.contact_delete_question),
+ name
+ )
+ positiveText.value = ctx.getString(R.string.delete)
+ positiveAction.value = {
+ val id = contact.id
+ val avatarFile = File(
+ BaresipService.filesPath,
+ "$id.png"
+ )
+ if (avatarFile.exists()) {
+ try {
+ avatarFile.delete()
+ } catch (e: IOException) {
+ Log.e(
+ TAG,
+ "Could not delete file $id.png: ${e.message}"
+ )
+ }
+ }
+ Contact.removeBaresipContact(contact)
+ }
+ showDialog.value = true
+ }
+ )
+ )
+ SmallFloatingActionButton(
+ modifier = Modifier.padding(end = 10.dp),
+ onClick = { navController.navigate("baresip_contact/${name}/old") },
+ containerColor = LocalCustomColors.current.background,
+ contentColor = LocalCustomColors.current.secondary
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Edit,
+ modifier = Modifier.size(28.dp),
+ contentDescription = stringResource(R.string.edit)
+ )
+ }
+ }
+
+ is Contact.AndroidContact -> {
+ Text(text = name,
+ fontSize = 20.sp,
+ fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal,
+ color = LocalCustomColors.current.itemText,
+ modifier = Modifier
+ .weight(1f)
+ .padding(start = 10.dp, top = 4.dp, bottom = 4.dp)
+ .combinedClickable(
+ onClick = { navController.navigate("android_contact/${name}") },
+ onLongClick = {
+ dialogMessage.value = String.format(
+ ctx.getString(R.string.contact_delete_question),
+ name
+ )
+ positiveText.value = ctx.getString(R.string.delete)
+ positiveAction.value = {
+ ctx.contentResolver.delete(
+ ContactsContract.RawContacts.CONTENT_URI,
+ ContactsContract.Contacts.DISPLAY_NAME + "='" + name + "'",
+ null
+ )
+ }
+ neutralText.value = ""
+ showDialog.value = true
+ }
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/tutpro/baresip/CustomElements.kt b/app/src/main/kotlin/com/tutpro/baresip/CustomElements.kt
index d92d1ef3..2ad930b4 100644
--- a/app/src/main/kotlin/com/tutpro/baresip/CustomElements.kt
+++ b/app/src/main/kotlin/com/tutpro/baresip/CustomElements.kt
@@ -5,6 +5,7 @@ import android.graphics.Bitmap
import android.widget.Toast
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
+import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollState
@@ -117,12 +118,14 @@ object CustomElements {
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
shape: Shape,
+ border: BorderStroke? = null,
color: Color,
content: @Composable RowScope.() -> Unit
) {
Surface(
shape = shape,
color = color,
+ border = border,
modifier = modifier
.pointerInput(Unit) {
detectTapGestures(
@@ -491,7 +494,7 @@ object CustomElements {
BasicAlertDialog(
properties = DialogProperties(
dismissOnBackPress = false,
- dismissOnClickOutside = false
+ dismissOnClickOutside = false,
),
onDismissRequest = {
keyboardController?.hide()
diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt
index c33df5b9..0046e13f 100644
--- a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt
+++ b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt
@@ -17,140 +17,24 @@ import android.content.Intent.ACTION_CALL
import android.content.Intent.ACTION_DIAL
import android.content.Intent.ACTION_VIEW
import android.content.IntentFilter
-import android.content.pm.PackageManager
-import android.content.res.Configuration
-import android.content.res.Configuration.ORIENTATION_PORTRAIT
import android.media.AudioManager
-import android.net.Uri
import android.os.Build
import android.os.Bundle
-import android.os.Handler
-import android.os.Looper
-import android.os.Process
-import android.os.SystemClock
-import android.provider.DocumentsContract
-import android.provider.MediaStore
import android.view.KeyEvent
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
-import android.widget.Chronometer
-import android.widget.Toast
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
-import androidx.annotation.RequiresApi
-import androidx.appcompat.app.AppCompatDelegate
-import androidx.compose.animation.animateContentSize
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.gestures.detectHorizontalDragGestures
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.KeyboardArrowDown
-import androidx.compose.material.icons.filled.KeyboardArrowUp
-import androidx.compose.material.icons.filled.Menu
-import androidx.compose.material.icons.outlined.Clear
-import androidx.compose.material3.BasicAlertDialog
-import androidx.compose.material3.ButtonColors
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.OutlinedTextFieldDefaults
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Switch
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.material3.TextField
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator
-import androidx.compose.material3.pulltorefresh.pullToRefresh
-import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
-import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalSoftwareKeyboardController
-import androidx.compose.ui.platform.SoftwareKeyboardController
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.res.vectorResource
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.core.content.ContextCompat
-import androidx.core.net.toUri
import androidx.lifecycle.Observer
-import com.tutpro.baresip.BaresipService.Companion.contactNames
-import com.tutpro.baresip.BaresipService.Companion.uas
-import com.tutpro.baresip.BaresipService.Companion.uasStatus
-import com.tutpro.baresip.CustomElements.AlertDialog
-import com.tutpro.baresip.CustomElements.DropdownMenu
-import com.tutpro.baresip.CustomElements.LabelText
-import com.tutpro.baresip.CustomElements.PasswordDialog
-import com.tutpro.baresip.CustomElements.SelectableAlertDialog
-import com.tutpro.baresip.CustomElements.verticalScrollbar
-import kotlinx.coroutines.delay
-import java.io.File
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.rememberNavController
import kotlin.system.exitProcess
class MainActivity : ComponentActivity() {
@@ -163,100 +47,16 @@ class MainActivity : ComponentActivity() {
private lateinit var serviceEventObserver: Observer>
private lateinit var requestPermissionLauncher: ActivityResultLauncher
private lateinit var requestPermissionsLauncher: ActivityResultLauncher>
- private lateinit var accountsRequest: ActivityResultLauncher
- private lateinit var chatRequests: ActivityResultLauncher
- private lateinit var configRequest: ActivityResultLauncher
- private lateinit var backupRequest: ActivityResultLauncher
- private lateinit var restoreRequest: ActivityResultLauncher
- private lateinit var logcatRequest: ActivityResultLauncher
- private lateinit var contactsRequest: ActivityResultLauncher
- private lateinit var callsRequest: ActivityResultLauncher
- private lateinit var accountRequest: ActivityResultLauncher
private lateinit var comDevChangedListener: AudioManager.OnCommunicationDeviceChangedListener
- private lateinit var permissions: Array
private lateinit var baresipService: Intent
- private var callHandler: Handler = Handler(Looper.getMainLooper())
- private var callRunnable: Runnable? = null
- private var downloadsInputUri: Uri? = null
- private var downloadsOutputUri: Uri? = null
- private var audioModeChangedListener: AudioManager.OnModeChangedListener? = null
- private var keyboardController: SoftwareKeyboardController? = null
-
private var restart = false
private var atStartup = false
private var initialized = false
- private var resumeUri = ""
- private var resumeUap = 0L
- private var resumeCall: Call? = null
- private var resumeAction = ""
-
private val viewModel: ViewModel by viewModels()
-
- private var callUri = mutableStateOf("")
- private var callUriEnabled = mutableStateOf(true)
- private var callUriLabel = mutableStateOf("")
- private var securityIcon = mutableIntStateOf(-1)
- private var callTimer: Chronometer? = null
- private var showCallTimer = mutableStateOf(false)
- private var showSuggestions = mutableStateOf(false)
- private var showCallButton = mutableStateOf(true)
- private var showCancelButton = mutableStateOf(false)
- private var showAnswerRejectButtons = mutableStateOf(false)
- private var showHangupButton = mutableStateOf(false)
- private var showOnHoldNotice = mutableStateOf(false)
- private var showPasswordDialog = mutableStateOf(false)
- private var password = mutableStateOf("")
- private var showPasswordsDialog = mutableStateOf(false)
- private var holdIcon = mutableIntStateOf(R.drawable.call_hold)
- private var transferButtonEnabled = mutableStateOf(false)
- private var transferIcon = mutableIntStateOf(R.drawable.call_transfer)
- private var dtmfText = mutableStateOf("")
- private var dtmfEnabled = mutableStateOf(false)
- private var focusDtmf = mutableStateOf(false)
- private var showVmIcon by mutableStateOf(false)
- private var vmIcon = mutableIntStateOf(R.drawable.voicemail)
- private var messagesIcon = mutableIntStateOf(R.drawable.messages)
- private var callsIcon = mutableIntStateOf(R.drawable.calls)
- private var micIcon by mutableIntStateOf(R.drawable.mic_on)
- private var speakerIcon by mutableIntStateOf(R.drawable.speaker_off)
- private var dialpad by mutableStateOf(false)
- private var dialpadButtonEnabled by mutableStateOf(true)
- private var pullToRefreshEnabled by mutableStateOf(true)
- private var passwordAccounts = mutableListOf()
- private var passwordTitle by mutableStateOf("")
-
- private val alertTitle = mutableStateOf("")
- private val alertMessage = mutableStateOf("")
- private val showAlert = mutableStateOf(false)
-
- private val dialogTitle = mutableStateOf("")
- private val dialogMessage = mutableStateOf("")
- private val positiveText = mutableStateOf("")
- private val onPositiveClicked = mutableStateOf({})
- private val negativeText = mutableStateOf("")
- private val onNegativeClicked = mutableStateOf({})
- private val showDialog = mutableStateOf(false)
-
- private val showSelectItemDialog = mutableStateOf(false)
- val items = mutableStateOf(listOf())
- private val itemAction = mutableStateOf<(Int) -> Unit>({ _ -> run {} })
-
- private var backInvokedCallback: OnBackInvokedCallback? = null
- private lateinit var onBackPressedCallback: OnBackPressedCallback
-
- @RequiresApi(33)
- private fun registerBackInvokedCallback() {
- backInvokedCallback = OnBackInvokedCallback {
- moveTaskToBack(true)
- }
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- backInvokedCallback!!
- )
- }
+ private lateinit var navController: NavHostController
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
@@ -265,19 +65,8 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
- registerBackInvokedCallback()
- else {
- onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- moveTaskToBack(true)
- }
- }
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
val extraAction = intent.getStringExtra("action")
- Log.d(TAG, "Main onCreate ${intent.action}/${intent.data}/$extraAction")
+ Log.e(TAG, "Main onCreate ${intent.action}/${intent.data}/$extraAction")
window.addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES)
@@ -298,7 +87,35 @@ class MainActivity : ComponentActivity() {
Log.d(TAG, "Observed event $event")
if (event != null && BaresipService.serviceEvents.isNotEmpty()) {
val first = BaresipService.serviceEvents.removeAt(0)
- handleServiceEvent(first.event, first.params)
+ if (taskId != -1) {
+ if (first.event == "started" && !initialized)
+ // Android has restarted baresip when permission has been denied in app settings
+ recreate()
+ else {
+ if (first.event == "stopped") {
+ Log.d(
+ TAG,
+ "Handling service event 'stopped' with start error '${first.params[0]}'"
+ )
+ if (first.params[0] != "")
+ handleDialog(
+ ctx = applicationContext,
+ title = getString(R.string.notice),
+ message =getString(R.string.start_failed)
+ )
+ else {
+ finishAndRemoveTask()
+ if (restart)
+ reStart()
+ else
+ exitProcess(0)
+ }
+ } else
+ handleServiceEvent(applicationContext, viewModel, first.event, first.params)
+ }
+ }
+ else
+ Log.d(TAG, "Omit service event '$event' for task -1")
}
}
@@ -321,10 +138,11 @@ class MainActivity : ComponentActivity() {
comDevChangedListener = AudioManager.OnCommunicationDeviceChangedListener { device ->
if (device != null) {
Log.d(TAG, "Com device changed to type ${device.type} in mode ${am.mode}")
- speakerIcon = if (Utils.isSpeakerPhoneOn(am))
+ val speakerIcon = if (Utils.isSpeakerPhoneOn(am))
R.drawable.speaker_on
else
R.drawable.speaker_off
+ viewModel.updateSpeakerIcon(speakerIcon)
}
}
am.addOnCommunicationDeviceChangedListener(mainExecutor, comDevChangedListener)
@@ -332,113 +150,47 @@ class MainActivity : ComponentActivity() {
initialized = true
- accountsRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (Account.ofAor(activityAor) != null)
- spinToAor(activityAor)
- else {
- if (Account.ofAor(viewModel.selectedAor.value) == null) {
- if (uas.value.isNotEmpty())
- viewModel.updateSelectedAor(uas.value.first().account.aor)
- else
- viewModel.updateSelectedAor("")
- }
- updateIcons(Account.ofAor(viewModel.selectedAor.value))
- }
- if (BaresipService.isServiceRunning) {
- baresipService.action = "Update Notification"
- startService(baresipService)
- }
- }
-
- accountRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- spinToAor(activityAor)
- val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
- updateIcons(ua.account)
- }
-
- contactsRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- }
-
- chatRequests = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- spinToAor(activityAor)
- updateIcons(Account.ofAor(activityAor)!!)
- }
-
- callsRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- spinToAor(activityAor)
- updateIcons(Account.ofAor(viewModel.selectedAor.value))
- }
-
- configRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if ((it.data != null) && it.data!!.hasExtra("restart")) {
- dialogTitle.value = getString(R.string.restart_request)
- dialogMessage.value = getString(R.string.config_restart)
- positiveText.value = getString(R.string.restart)
- onPositiveClicked.value = {
- quitRestart(true)
- }
- negativeText.value = getString(R.string.cancel)
- showDialog.value = true
- }
- val displayTheme = Preferences(applicationContext).displayTheme
- if (displayTheme != AppCompatDelegate.getDefaultNightMode()) {
- AppCompatDelegate.setDefaultNightMode(displayTheme)
- }
- }
-
- backupRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == RESULT_OK)
- it.data?.data?.also { uri ->
- downloadsOutputUri = uri
- passwordTitle = getString(R.string.encrypt_password)
- showPasswordDialog.value = true
- }
- }
-
- restoreRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == RESULT_OK)
- it.data?.data?.also { uri ->
- downloadsInputUri = uri
- passwordTitle = getString(R.string.decrypt_password)
- showPasswordDialog.value = true
- }
- }
-
- logcatRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == RESULT_OK)
- it.data?.data?.also { uri ->
- try {
- val out = contentResolver.openOutputStream(uri)
- val process = Runtime.getRuntime().exec("logcat -d --pid=${Process.myPid()}")
- val bufferedReader = process.inputStream.bufferedReader()
- bufferedReader.forEachLine { line ->
- out!!.write(line.toByteArray())
- out.write('\n'.code.toByte().toInt())
- }
- out!!.close()
- } catch (e: Exception) {
- Log.e(TAG, "Failed to write logcat to file: $e")
- }
- }
- }
-
- micIcon = if (BaresipService.isMicMuted)
+ viewModel.updateMicIcon(if (BaresipService.isMicMuted)
R.drawable.mic_off
else
R.drawable.mic_on
+ )
- setContent {
- AppTheme {
- keyboardController = LocalSoftwareKeyboardController.current
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = LocalCustomColors.current.background
- ) {
- MainScreen(this)
+ val restartApp = {
+ Log.i(TAG, "Restarting baresip")
+ window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
+ WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
+ if (BaresipService.isServiceRunning) {
+ restart = true
+ baresipService.action = "Stop"
+ startService(baresipService)
+ } else {
+ finishAndRemoveTask()
+ val pm = applicationContext.packageManager
+ val intent = pm.getLaunchIntentForPackage(applicationContext.packageName)
+ if (intent != null) {
+ applicationContext.startActivity(intent)
+ exitProcess(0)
+ } else {
+ Log.e(TAG, "Failed to restart: Launch intent is null")
}
}
}
+ val quitApp = {
+ Log.i(TAG, "Quiting baresip")
+ window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
+ WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
+ if (BaresipService.isServiceRunning) {
+ restart = false
+ baresipService.action = "Stop"
+ startService(baresipService)
+ } else {
+ finishAndRemoveTask()
+ exitProcess(0)
+ }
+ }
+
baresipService = Intent(this@MainActivity, BaresipService::class.java)
atStartup = intent.hasExtra("onStartup")
@@ -446,21 +198,26 @@ class MainActivity : ComponentActivity() {
when (intent?.action) {
ACTION_DIAL, ACTION_CALL, ACTION_VIEW ->
if (BaresipService.isServiceRunning)
- callAction(intent.data, if (intent?.action == ACTION_CALL) "call" else "dial")
+ callAction(applicationContext, viewModel, intent.data, if (intent?.action == ACTION_CALL) "call" else "dial")
else
- BaresipService.callActionUri = intent.data.toString()
- .replace("tel:%2B", "tel:+")
+ BaresipService.callActionUri = intent.data.toString().replace("%2B", "+")
+ .replace("%20", "").filterNot{setOf('-', ' ', '(', ')').contains(it)}
}
- permissions = if (Build.VERSION.SDK_INT >= 33)
+ val permissions = if (Build.VERSION.SDK_INT >= 33)
arrayOf(POST_NOTIFICATIONS, RECORD_AUDIO, BLUETOOTH_CONNECT)
else if (Build.VERSION.SDK_INT >= 31)
arrayOf(RECORD_AUDIO, BLUETOOTH_CONNECT)
else
- arrayOf(RECORD_AUDIO)
+ if (Build.VERSION.SDK_INT < 29)
+ arrayOf(RECORD_AUDIO, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE)
+ else
+ arrayOf(RECORD_AUDIO)
requestPermissionLauncher =
- registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
+ registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
+ Log.i(TAG, "Permission granted: $isGranted")
+ }
requestPermissionsLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
@@ -474,1701 +231,121 @@ class MainActivity : ComponentActivity() {
}
}
if (denied.contains(POST_NOTIFICATIONS) && !shouldShow.contains(POST_NOTIFICATIONS)) {
- dialogTitle.value = getString(R.string.notice)
- dialogMessage.value = getString(R.string.no_notifications)
- positiveText.value = getString(R.string.ok)
- onPositiveClicked.value = {
- quitRestart(false)
- }
- negativeText.value = ""
- showDialog.value = true
+ handleDialog(
+ ctx = applicationContext,
+ title = getString(R.string.notice),
+ message = getString(R.string.no_notifications),
+ action = { quitRestart(false) }
+ )
} else {
if (shouldShow.isNotEmpty()) {
- dialogTitle.value = getString(R.string.permissions_rationale)
- dialogMessage.value = getString(R.string.audio_permissions)
- positiveText.value = getString(R.string.ok)
- onPositiveClicked.value = {
- requestPermissionsLauncher.launch(permissions)
- }
- negativeText.value = ""
- showDialog.value = true
+ handleDialog(
+ ctx = applicationContext,
+ title = getString(R.string.permissions_rationale),
+ message = getString(R.string.audio_permissions),
+ action = { requestPermissionsLauncher.launch(permissions) }
+ )
+ }
+ else {
+ if (!BaresipService.isStartReceived) {
+ baresipService.action = "Start"
+ startService(baresipService)
+ if (atStartup)
+ moveTaskToBack(true)
+ }
}
- else
- startBaresip()
}
}
- if (!BaresipService.isServiceRunning) {
- if (File(filesDir.absolutePath + "/accounts").exists()) {
- passwordAccounts = String(
- Utils.getFileContents(filesDir.absolutePath + "/accounts")!!,
- Charsets.UTF_8
- ).lines().toMutableList()
- showPasswordsDialog.value = true
- } else {
- // Baresip is started for the first time
- requestPermissionsLauncher.launch(permissions)
+ setContent {
+
+ AppTheme {
+
+ navController = rememberNavController()
+
+ LaunchedEffect(key1 = viewModel, key2 = navController) {
+ viewModel.navigationCommand.collect { command ->
+ Log.d(TAG, "MainActivity: Received NavigationCommand: $command")
+ when (command) {
+ is NavigationCommand.NavigateToChat -> {
+ val route = "chat/${command.aor}/${command.peer}"
+ navController.navigate(route) {
+ launchSingleTop = true
+ popUpTo("main")
+ }
+ }
+ is NavigationCommand.NavigateToCalls -> {
+ val route = "calls/${command.aor}"
+ navController.navigate(route) {
+ launchSingleTop = true
+ popUpTo("main")
+ }
+ }
+ is NavigationCommand.NavigateToHome -> {
+ navController.navigate("main") {
+ launchSingleTop = true
+ popUpTo("main")
+ }
+ }
+ }
+ }
+ }
+
+ NavHost(navController, startDestination = "main") {
+ mainScreenRoute(
+ navController = navController,
+ viewModel = viewModel,
+ onRequestPermissions = { requestPermissionsLauncher.launch(permissions) },
+ onRestartApp = { restartApp() },
+ onQuitApp = { quitApp() }
+ )
+ aboutScreenRoute(navController)
+ settingsScreenRoute(
+ navController = navController,
+ onRestartApp = { restartApp() }
+ )
+ accountsScreenRoute(navController)
+ audioScreenRoute(navController)
+ accountScreenRoute(navController)
+ codecsScreenRoute(navController)
+ contactsScreenRoute(navController, viewModel)
+ baresipContactScreenRoute(navController)
+ androidContactScreenRoute(navController, viewModel)
+ callsScreenRoute(navController, viewModel)
+ callDetailsScreenRoute(navController, viewModel)
+ chatsScreenRoute(navController)
+ chatScreenRoute(navController, viewModel)
+ }
}
}
} // OnCreate
- @Composable
- private fun MainScreen(ctx: Context) {
- Scaffold(
- modifier = Modifier.safeDrawingPadding(),
- containerColor = LocalCustomColors.current.background,
- topBar = { TopAppBar(ctx, getString(R.string.baresip)) },
- bottomBar = { BottomBar(ctx) },
- content = { contentPadding ->
- MainContent(ctx, contentPadding)
- }
- )
- }
-
- @Composable
- fun MainContent(ctx: Context, contentPadding: PaddingValues) {
-
- var isRefreshing by remember { mutableStateOf(false) }
- val refreshState = rememberPullToRefreshState()
- var offset by remember { mutableFloatStateOf(0f) }
- val swipeThreshold = 200
-
- LaunchedEffect(isRefreshing) {
- if (isRefreshing) {
- delay(1000)
- isRefreshing = false
- }
- }
-
- if (showAlert.value)
- AlertDialog(
- showDialog = showAlert,
- title = alertTitle.value,
- message = alertMessage.value,
- positiveButtonText = stringResource(R.string.ok),
- )
-
- if (showDialog.value)
- AlertDialog(
- showDialog = showDialog,
- title = stringResource(R.string.confirmation),
- message = dialogMessage.value,
- positiveButtonText = positiveText.value,
- onPositiveClicked = onPositiveClicked.value,
- negativeButtonText = stringResource(R.string.cancel),
- onNegativeClicked = onNegativeClicked.value
- )
-
- SelectableAlertDialog(
- openDialog = showSelectItemDialog,
- title = stringResource(R.string.choose_destination_uri),
- items = items.value,
- onItemClicked = itemAction.value,
- neutralButtonText = stringResource(R.string.cancel),
- onNeutralClicked = {}
- )
-
- Column(
- modifier = Modifier
- .imePadding()
- .fillMaxWidth()
- .padding(contentPadding)
- .padding(top = 18.dp, bottom = 6.dp, start = 16.dp, end = 16.dp)
- .fillMaxSize()
- .pullToRefresh(
- state = refreshState,
- isRefreshing = isRefreshing,
- onRefresh = {
- isRefreshing = true
- if (uas.value.isNotEmpty()) {
- if (viewModel.selectedAor.value == "")
- spinToAor(uas.value.first().account.aor)
- val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
- if (ua.account.regint > 0)
- Api.ua_register(ua.uap)
- }
- },
- enabled = pullToRefreshEnabled,
- )
- .pointerInput(Unit) {
- detectHorizontalDragGestures(
- onDragStart = { offset = 0f },
- onDragEnd = {
- if (offset < -swipeThreshold) {
- if (uas.value.isNotEmpty()) {
- val curPos = UserAgent.findAorIndex(viewModel.selectedAor.value)
- val newPos = if (curPos == null)
- 0
- else
- (curPos + 1) % uas.value.size
- if (curPos != newPos) {
- val ua = uas.value[newPos]
- spinToAor(ua.account.aor)
- showCall(ua)
- }
- }
- } else if (offset > swipeThreshold) {
- if (uas.value.isNotEmpty()) {
- val curPos = UserAgent.findAorIndex(viewModel.selectedAor.value)
- val newPos = when (curPos) {
- null -> 0
- 0 -> uas.value.size - 1
- else -> curPos - 1
- }
- if (curPos != newPos) {
- val ua = uas.value[newPos]
- spinToAor(ua.account.aor)
- showCall(ua)
- }
- }
- }
- }
- ) { _, dragAmount ->
- offset += dragAmount
- }
- }
- .verticalScroll(rememberScrollState()),
- verticalArrangement = Arrangement.Top,
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- AccountSpinner(ctx)
- CallUriRow()
- CallRow(ctx)
- OnHoldNotice()
- AskPasswords(ctx)
- AskPassword(ctx)
- Indicator(
- modifier = Modifier.align(Alignment.CenterHorizontally),
- isRefreshing = isRefreshing,
- state = refreshState,
- )
- }
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- fun TopAppBar(ctx: Context, title: String) {
-
- val recOffImage = ImageVector.vectorResource(R.drawable.rec_off)
- val recOnImage = ImageVector.vectorResource(R.drawable.rec_on)
-
- var recImage by remember { mutableStateOf(recOffImage) }
- var menuExpanded by remember { mutableStateOf(false) }
-
- val about = stringResource(R.string.about)
- val settings = stringResource(R.string.configuration)
- val accounts = stringResource(R.string.accounts)
- val backup = stringResource(R.string.backup)
- val restore = stringResource(R.string.restore)
- val logcat = stringResource(R.string.logcat)
- val restart = stringResource(R.string.restart)
- val quit = stringResource(R.string.quit)
-
- TopAppBar(
- title = {
- Text(
- text = title,
- color = LocalCustomColors.current.light,
- fontSize = 22.sp,
- fontWeight = FontWeight.Bold
- )
- },
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = LocalCustomColors.current.primary
- ),
- actions = {
-
- Icon(
- imageVector = recImage,
- modifier = Modifier.size(40.dp).combinedClickable(
- onClick = {
- if (Call.call("connected") == null) {
- BaresipService.isRecOn = !BaresipService.isRecOn
- recImage = if (BaresipService.isRecOn) {
- Api.module_load("sndfile")
- recOnImage
- } else {
- Api.module_unload("sndfile")
- recOffImage
- }
- } else
- Toast.makeText(ctx, R.string.rec_in_call, Toast.LENGTH_SHORT)
- .show()
- },
- onLongClick = {
- alertTitle.value = getString(R.string.call_recording_title)
- alertMessage.value = getString(R.string.call_recording_tip)
- showAlert.value = true
- }
- ),
- tint = Color.Unspecified,
- contentDescription = null
- )
-
- Spacer(modifier = Modifier.width(22.dp))
-
- Icon(
- imageVector = ImageVector.vectorResource(micIcon),
- modifier = Modifier
- .size(40.dp)
- .combinedClickable(
- onClick = {
- if (Call.call("connected") != null) {
- BaresipService.isMicMuted = !BaresipService.isMicMuted
- if (BaresipService.isMicMuted) {
- micIcon = R.drawable.mic_off
- Api.calls_mute(true)
- } else {
- micIcon = R.drawable.mic_on
- Api.calls_mute(false)
- }
- }
- },
- onLongClick = {
- alertTitle.value = getString(R.string.microphone_title)
- alertMessage.value = getString(R.string.microphone_tip)
- showAlert.value = true
- },
- ),
- tint = Color.Unspecified,
- contentDescription = null
- )
-
- Spacer(modifier = Modifier.width(16.dp))
-
- Icon(
- imageVector = ImageVector.vectorResource(speakerIcon),
- modifier = Modifier
- .size(40.dp)
- .combinedClickable(
- onClick = {
- if (Build.VERSION.SDK_INT >= 31)
- Log.d(
- TAG, "Toggling speakerphone when dev/mode is " +
- "${am.communicationDevice!!.type}/${am.mode}"
- )
- Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am)
- speakerIcon = if (Utils.isSpeakerPhoneOn(am))
- R.drawable.speaker_on
- else
- R.drawable.speaker_off
- },
- onLongClick = {
- alertTitle.value = getString(R.string.speakerphone_title)
- alertMessage.value = getString(R.string.speakerphone_tip)
- showAlert.value = true
- },
- ),
- tint = Color.Unspecified,
- contentDescription = null
- )
-
- Spacer(modifier = Modifier.width(8.dp))
-
- IconButton(
- onClick = { menuExpanded = !menuExpanded }
- ) {
- Icon(
- imageVector = Icons.Filled.Menu,
- contentDescription = "Menu",
- tint = LocalCustomColors.current.light
- )
- }
-
- DropdownMenu(
- expanded = menuExpanded,
- onDismissRequest = { menuExpanded = false },
- items = if (Build.VERSION.SDK_INT >= 29)
- listOf(about, settings, accounts, backup, restore, logcat, restart, quit)
- else
- listOf(about, settings, accounts, backup, restore, restart, quit),
- onItemClick = { selectedItem ->
- menuExpanded = false
- when (selectedItem) {
- about -> {
- startActivity(Intent(ctx, AboutActivity::class.java))
- }
- settings -> {
- configRequest.launch(Intent(ctx, ConfigActivity::class.java))
- }
- accounts -> {
- val i = Intent(ctx, AccountsActivity::class.java)
- val b = Bundle()
- b.putString("aor", viewModel.selectedAor.value)
- i.putExtras(b)
- accountsRequest.launch(i)
- }
- backup -> {
- when {
- Build.VERSION.SDK_INT >= 29 ->
- pickupFileFromDownloads("backup")
- ContextCompat.checkSelfPermission(ctx, WRITE_EXTERNAL_STORAGE) ==
- PackageManager.PERMISSION_GRANTED -> {
- Log.d(TAG, "Write External Storage permission granted")
- val path = Utils.downloadsPath("baresip.bs")
- downloadsOutputUri = File(path).toUri()
- passwordTitle = getString(R.string.encrypt_password)
- showPasswordDialog.value = true
- }
- shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE) -> {
- dialogTitle.value = getString(R.string.notice)
- dialogMessage.value = getString(R.string.no_backup)
- positiveText.value = getString(R.string.ok)
- onPositiveClicked.value = {
- requestPermissionLauncher.launch(WRITE_EXTERNAL_STORAGE)
- }
- negativeText.value = ""
- showDialog.value = true
- }
- else ->
- requestPermissionLauncher.launch(WRITE_EXTERNAL_STORAGE)
- }
- }
- restore -> {
- when {
- Build.VERSION.SDK_INT >= 29 ->
- pickupFileFromDownloads("restore")
- ContextCompat.checkSelfPermission(ctx, READ_EXTERNAL_STORAGE) ==
- PackageManager.PERMISSION_GRANTED -> {
- Log.d(TAG, "Read External Storage permission granted")
- val path = Utils.downloadsPath("baresip.bs")
- downloadsInputUri = File(path).toUri()
- passwordTitle = getString(R.string.decrypt_password)
- showPasswordDialog.value = true
- }
- shouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE) -> {
- dialogTitle.value = getString(R.string.notice)
- dialogMessage.value = getString(R.string.no_restore)
- positiveText.value = getString(R.string.ok)
- onPositiveClicked.value = {
- requestPermissionLauncher.launch(READ_EXTERNAL_STORAGE)
- }
- negativeText.value = ""
- showDialog.value = true
- }
- else ->
- requestPermissionLauncher.launch(READ_EXTERNAL_STORAGE)
- }
- }
- logcat -> {
- if (Build.VERSION.SDK_INT >= 29)
- pickupFileFromDownloads("logcat")
- }
- restart -> {
- quitRestart(true)
- }
- quit -> {
- quitRestart(false)
- }
- }
- }
- )
- }
- )
- }
-
- @OptIn(ExperimentalFoundationApi::class)
- @Composable
- fun AccountSpinner(ctx: Context) {
-
- var expanded by rememberSaveable { mutableStateOf(false) }
- val selected: String by viewModel.selectedAor.collectAsState()
-
- if (uas.value.isEmpty())
- viewModel.updateSelectedAor("")
- else
- if (selected == "" || UserAgent.ofAor(selected) == null) {
- viewModel.updateSelectedAor(uas.value.first().account.aor)
- }
-
- showCall(UserAgent.ofAor(selected))
- updateIcons(Account.ofAor(selected))
-
- if (selected == "") {
- OutlinedButton(
- onClick = {
- val i = Intent(this@MainActivity, AccountsActivity::class.java)
- val b = Bundle()
- b.putString("aor", selected)
- i.putExtras(b)
- accountsRequest.launch(i)
- },
- modifier = Modifier
- .padding(horizontal = 4.dp)
- .height(50.dp)
- .fillMaxWidth(),
- colors = ButtonColors(
- containerColor = LocalCustomColors.current.grayLight,
- contentColor = LocalCustomColors.current.dark,
- disabledContainerColor = LocalCustomColors.current.grayLight,
- disabledContentColor = LocalCustomColors.current.dark
- ),
- shape = RoundedCornerShape(12.dp),
- contentPadding = PaddingValues(horizontal = 10.dp)
- ) {
- Text(text = "")
- }
- }
- else
- OutlinedButton(
- onClick = {
- expanded = !expanded
- },
- enabled = true,
- modifier = Modifier
- .padding(horizontal = 4.dp)
- .height(50.dp)
- .pointerInput(Unit) {
- detectTapGestures(
- onPress = {
- expanded = true
- },
- onLongPress = {
- val ua = UserAgent.ofAor(selected)
- if (ua != null) {
- val acc = ua.account
- if (Api.account_regint(acc.accp) > 0) {
- Api.account_set_regint(acc.accp, 0)
- Api.ua_unregister(ua.uap)
- } else {
- Api.account_set_regint(
- acc.accp,
- acc.configuredRegInt
- )
- Api.ua_register(ua.uap)
- }
- acc.regint = Api.account_regint(acc.accp)
- AccountsActivity.saveAccounts()
- }
- }
- )
- },
- colors = ButtonColors(
- containerColor = LocalCustomColors.current.grayLight,
- contentColor = LocalCustomColors.current.dark,
- disabledContainerColor = LocalCustomColors.current.grayLight,
- disabledContentColor = LocalCustomColors.current.dark
- ),
- shape = RoundedCornerShape(12.dp),
- contentPadding = PaddingValues(horizontal = 10.dp)
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(
- uasStatus.value[selected] ?:
- R.drawable.locked_yellow),
- contentDescription = null,
- tint = Color.Unspecified,
- modifier = Modifier
- .padding(end = 10.dp)
- .clickable(onClick = {
- val i = Intent(ctx, AccountActivity::class.java)
- val b = Bundle()
- b.putString("aor", selected)
- i.putExtras(b)
- accountRequest.launch(i)
- })
- )
- Text(
- text = Account.ofAor(selected)?.text() ?: "",
- fontSize = 17.sp,
- fontWeight = FontWeight.Bold,
- overflow = TextOverflow.Ellipsis,
- maxLines = 1,
- modifier = Modifier
- .weight(1f)
- .combinedClickable(
- onClick = { expanded = true },
- onLongClick = {
- val ua = UserAgent.ofAor(selected)
- if (ua != null) {
- val acc = ua.account
- if (Api.account_regint(acc.accp) > 0) {
- Api.account_set_regint(acc.accp, 0)
- Api.ua_unregister(ua.uap)
- } else {
- Api.account_set_regint(
- acc.accp,
- acc.configuredRegInt
- )
- Api.ua_register(ua.uap)
- }
- acc.regint = Api.account_regint(acc.accp)
- AccountsActivity.saveAccounts()
- }
- }
- )
- )
- Icon(
- imageVector = if (expanded)
- Icons.Default.KeyboardArrowUp
- else
- Icons.Default.KeyboardArrowDown,
- contentDescription = null
- )
- androidx.compose.material3.DropdownMenu(
- expanded = expanded,
- onDismissRequest = { expanded = false },
- ) {
- uas.value.forEachIndexed { _, ua ->
- val acc = ua.account
- DropdownMenuItem(
- onClick = {
- expanded = false
- run {
- viewModel.updateSelectedAor(acc.aor)
- }
- showCall(ua)
- updateIcons(acc)
- },
- text = { Text(
- text = acc.text(),
- fontSize = 17.sp,
- fontWeight = FontWeight.Bold
- ) },
- leadingIcon = {
- Icon(
- imageVector = ImageVector.vectorResource(uasStatus.value[acc.aor]!!),
- contentDescription = null,
- tint = Color.Unspecified,
- )
- }
- )
- }
- }
- }
- }
-
- @Composable
- fun CallUriRow() {
- val suggestions by remember { contactNames }
- var filteredSuggestions by remember { mutableStateOf(suggestions) }
- val focusRequester = remember { FocusRequester() }
- val lazyListState = rememberLazyListState()
-
- Row(modifier = Modifier
- .fillMaxWidth()
- .padding(top = 4.dp, bottom = 8.dp),
- verticalAlignment = Alignment.CenterVertically) {
- Column(
- modifier = Modifier.weight(1f),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- OutlinedTextField(
- value = callUri.value,
- enabled = callUriEnabled.value,
- singleLine = true,
- colors = OutlinedTextFieldDefaults.colors(
- disabledBorderColor = OutlinedTextFieldDefaults.colors().unfocusedIndicatorColor),
- onValueChange = {
- if (it != callUri.value) {
- callUri.value = it
- filteredSuggestions = suggestions.filter { suggestion ->
- it.length > 2 && suggestion.startsWith(it, ignoreCase = true)
- }
- showSuggestions.value = it.length > 2
- }
- },
- trailingIcon = {
- if (callUriEnabled.value && callUri.value.isNotEmpty())
- Icon(Icons.Outlined.Clear,
- contentDescription = null,
- modifier = Modifier
- .clickable {
- if (showSuggestions.value)
- showSuggestions.value = false
- else
- callUri.value = ""
- }
- )
- },
- modifier = Modifier
- .fillMaxWidth()
- .padding(start = 4.dp, end = 4.dp, top = 12.dp, bottom = 2.dp)
- .focusRequester(focusRequester)
- .onFocusChanged {
- val account = Account.ofAor(viewModel.selectedAor.value)
- if (account != null) {
- dialpad = account.numericKeypad
- }
- },
- label = {
- LabelText(
- text = callUriLabel.value,
- fontSize = 18.sp,
- )
- },
- textStyle = TextStyle(
- fontSize = 18.sp,
- color = LocalCustomColors.current.itemText
- ),
- keyboardOptions = if (dialpad)
- KeyboardOptions(keyboardType = KeyboardType.Phone)
- else
- KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- Spacer(modifier = Modifier.height(8.dp))
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .shadow(8.dp, RoundedCornerShape(8.dp))
- .background(
- LocalCustomColors.current.grayLight,
- shape = RoundedCornerShape(8.dp)
- )
- .animateContentSize()
- ) {
- if (showSuggestions.value && filteredSuggestions.isNotEmpty()) {
- Box(modifier = Modifier
- .fillMaxWidth()
- .heightIn(max = 150.dp)) {
- LazyColumn(
- modifier = Modifier
- .fillMaxWidth()
- .verticalScrollbar(
- state = lazyListState,
- color = LocalCustomColors.current.gray
- ),
- horizontalAlignment = Alignment.Start,
- state = lazyListState
- ) {
- items(
- items = filteredSuggestions,
- key = { suggestion -> suggestion }
- ) { suggestion ->
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- callUri.value = suggestion
- showSuggestions.value = false
- }
- .padding(12.dp)
- ) {
- Text(
- text = suggestion,
- modifier = Modifier.fillMaxWidth(),
- color = LocalCustomColors.current.grayDark,
- fontSize = 18.sp
- )
- }
- }
- }
- }
- }
- }
- }
- if (showCallTimer.value) {
- val textColor = LocalCustomColors.current.itemText.toArgb()
- AndroidView(
- factory = { context ->
- Chronometer(context).also { callTimer = it
- callTimer?.textSize = 16F
- callTimer?.setTextColor(textColor)
- }
- },
- modifier = Modifier.padding(start = 6.dp,
- top = 4.dp,
- end = if (securityIcon.intValue != -1) 6.dp else 0.dp),
- )
- }
- if (securityIcon.intValue != -1) {
- Icon(
- imageVector = ImageVector.vectorResource(securityIcon.intValue),
- contentDescription = null,
- modifier = Modifier
- .size(28.dp)
- .padding(top = 4.dp)
- .clickable {
- when (securityIcon.intValue) {
- R.drawable.unlocked -> {
- alertTitle.value = getString(R.string.alert)
- alertMessage.value = getString(R.string.call_not_secure)
- showAlert.value = true
- }
-
- R.drawable.locked_yellow -> {
- alertTitle.value = getString(R.string.alert)
- alertMessage.value = getString(R.string.peer_not_verified)
- showAlert.value = true
- }
-
- R.drawable.locked_green -> {
- dialogTitle.value = getString(R.string.info)
- dialogMessage.value = getString(R.string.call_is_secure)
- positiveText.value = getString(R.string.unverify)
- onPositiveClicked.value = {
- val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
- val call = ua.currentCall()
- if (call != null) {
- if (Api.cmd_exec("zrtp_unverify " + call.zid) != 0)
- Log.e(
- TAG,
- "Command 'zrtp_unverify ${call.zid}' failed"
- )
- else
- securityIcon.intValue = R.drawable.locked_yellow
- }
- }
- negativeText.value = getString(R.string.cancel)
- showDialog.value = true
- }
- }
- },
- tint = Color.Unspecified,
- )
- }
- }
- }
-
- @Composable
- fun CallRow(ctx: Context) {
-
- Row( modifier = Modifier
- .fillMaxWidth()
- .padding(start = 6.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Absolute.SpaceBetween
- ) {
- if (showCallButton.value)
- IconButton(
- modifier = Modifier.size(48.dp),
- onClick = {
- showSuggestions.value = false
- callClick(ctx)
- },
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(id = R.drawable.call),
- modifier = Modifier.size(48.dp),
- tint = Color.Unspecified,
- contentDescription = null,
- )
- }
-
- if (showCancelButton.value) {
- Spacer(modifier = Modifier.weight(1f))
- IconButton(
- modifier = Modifier.size(48.dp),
- onClick = {
- showSuggestions.value = false
- abandonAudioFocus()
- var ua: UserAgent = userAgentOfSelectedAor()
- val call = ua.currentCall()
- if (call != null) {
- val callp = call.callp
- Log.d(
- TAG,
- "AoR ${ua.account.aor} hanging up call $callp with ${callUri.value}"
- )
- Api.ua_hangup(ua.uap, callp, 0, "")
- }
- },
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(id = R.drawable.hangup),
- modifier = Modifier.size(48.dp),
- tint = Color.Unspecified,
- contentDescription = null,
- )
- }
- Spacer(modifier = Modifier.width(12.dp))
- }
-
- if (showHangupButton.value) {
-
- var ua: UserAgent = userAgentOfSelectedAor()
-
- IconButton(
- modifier = Modifier.size(48.dp),
- onClick = {
- abandonAudioFocus()
- val uaCalls = ua.calls()
- if (uaCalls.isNotEmpty()) {
- val call = uaCalls.last()
- val callp = call.callp
- Log.d(TAG, "AoR ${ua.account.aor} hanging up call $callp with ${callUri.value}")
- Api.ua_hangup(ua.uap, callp, 0, "")
- }
- }
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(id = R.drawable.hangup),
- modifier = Modifier.size(48.dp),
- tint = Color.Unspecified,
- contentDescription = null,
- )
- }
-
- IconButton(
- modifier = Modifier.size(48.dp),
- onClick = {
- val aor = ua.account.aor
- val call = ua.currentCall()
- if (call != null) {
- if (call.onhold) {
- Log.d(
- TAG,
- "AoR $aor resuming call ${call.callp} with ${callUri.value}"
- )
- call.resume()
- call.onhold = false
- holdIcon.intValue = R.drawable.call_hold
- } else {
- Log.d(
- TAG,
- "AoR $aor holding call ${call.callp} with ${callUri.value}"
- )
- call.hold()
- call.onhold = true
- holdIcon.intValue = R.drawable.resume
- }
- }
- },
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(id = holdIcon.intValue),
- modifier = Modifier.size(48.dp),
- tint = Color.Unspecified,
- contentDescription = null,
- )
- }
-
- var showTransferDialog by remember { mutableStateOf(false) }
- IconButton(
- modifier = Modifier.size(48.dp),
- enabled = transferButtonEnabled.value,
- onClick = {
- val call = ua.currentCall()
- if (call != null) {
- if (call.onHoldCall != null) {
- if (!call.executeTransfer()) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = getString(R.string.transfer_failed)
- showAlert.value = true
- }
- } else
- showTransferDialog = true
- }
- },
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(transferIcon.intValue),
- modifier = Modifier.size(48.dp),
- tint = Color.Unspecified,
- contentDescription = null,
- )
- }
-
- if (showTransferDialog) {
- val showDialog = remember { mutableStateOf(true) }
- val blindChecked = remember { mutableStateOf(true) }
- if (showDialog.value)
- BasicAlertDialog(
- onDismissRequest = {
- keyboardController?.hide()
- showDialog.value = false
- showTransferDialog = false
- }
- ) {
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 0.dp),
- shape = RoundedCornerShape(16.dp),
- colors = CardDefaults.cardColors(
- containerColor = LocalCustomColors.current.cardBackground
- )
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(
- text = stringResource(R.string.call_transfer),
- fontSize = 20.sp,
- color = LocalCustomColors.current.alert,
- )
- var transferUri by remember { mutableStateOf("") }
- val suggestions by remember { contactNames }
- var filteredSuggestions by remember { mutableStateOf(suggestions) }
- val focusRequester = remember { FocusRequester() }
- val lazyListState = rememberLazyListState()
- OutlinedTextField(
- value = transferUri,
- singleLine = true,
- onValueChange = {
- if (it != transferUri) {
- transferUri = it
- filteredSuggestions =
- suggestions.filter { suggestion ->
- transferUri.length > 2 &&
- suggestion.startsWith(
- transferUri,
- ignoreCase = true
- )
- }
- showSuggestions.value = transferUri.length > 2
- }
- },
- trailingIcon = {
- if (transferUri.isNotEmpty())
- Icon(
- Icons.Outlined.Clear,
- contentDescription = null,
- modifier = Modifier.clickable {
- if (showSuggestions.value)
- showSuggestions.value = false
- else
- transferUri = ""
- }
- )
- },
- modifier = Modifier
- .fillMaxWidth()
- .padding(
- start = 4.dp,
- end = 4.dp,
- top = 12.dp,
- bottom = 2.dp
- )
- .focusRequester(focusRequester),
- label = { LabelText(stringResource(R.string.transfer_destination)) },
- textStyle = TextStyle(
- fontSize = 18.sp,
- color = LocalCustomColors.current.itemText
- ),
- keyboardOptions = if (dialpad)
- KeyboardOptions(keyboardType = KeyboardType.Phone)
- else
- KeyboardOptions(keyboardType = KeyboardType.Text)
- )
- Spacer(modifier = Modifier.height(8.dp))
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .shadow(8.dp, RoundedCornerShape(8.dp))
- .background(
- LocalCustomColors.current.grayLight,
- shape = RoundedCornerShape(8.dp)
- )
- .animateContentSize()
- ) {
- if (showSuggestions.value && filteredSuggestions.isNotEmpty()) {
- Box(modifier = Modifier
- .fillMaxWidth()
- .heightIn(max = 150.dp)) {
- LazyColumn(
- modifier = Modifier
- .fillMaxWidth()
- .verticalScrollbar(
- state = lazyListState,
- color = LocalCustomColors.current.gray
- ),
- horizontalAlignment = Alignment.Start,
- state = lazyListState,
- ) {
- items(
- items = filteredSuggestions,
- key = { suggestion -> suggestion }
- ) { suggestion ->
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- transferUri = suggestion
- showSuggestions.value = false
- }
- .padding(12.dp)
- ) {
- Text(
- text = suggestion,
- modifier = Modifier.fillMaxWidth(),
- color = LocalCustomColors.current.grayDark,
- fontSize = 18.sp
- )
- }
- }
- }
- }
- }
- }
- val call = ua.currentCall()
- if (call != null && call.replaces())
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.Start,
- ) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = stringResource(R.string.blind),
- color = LocalCustomColors.current.alert,
- modifier = Modifier.padding(8.dp),
- )
- Switch(
- checked = blindChecked.value,
- onCheckedChange = {
- blindChecked.value = true
- }
- )
- }
- Spacer(modifier = Modifier.width(8.dp))
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = stringResource(R.string.attended),
- color = LocalCustomColors.current.alert,
- modifier = Modifier.padding(8.dp),
- )
- Switch(
- checked = !blindChecked.value,
- onCheckedChange = {
- blindChecked.value = false
- }
- )
- }
- }
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.End,
- verticalAlignment = Alignment.CenterVertically
- ) {
- TextButton(
- onClick = {
- keyboardController?.hide()
- showDialog.value = false
- showTransferDialog = false
- },
- modifier = Modifier.padding(end = 32.dp),
- ) {
- Text(
- text = stringResource(R.string.cancel),
- color = LocalCustomColors.current.gray
- )
- }
- TextButton(
- onClick = {
- showSuggestions.value = false
- var uriText = transferUri.trim()
- if (uriText.isNotEmpty()) {
- val uris = Contact.contactUris(uriText)
- if (uris.size > 1) {
- items.value = uris
- itemAction.value = { index ->
- val uri = uris[index]
- transfer(
- ua,
- if (Utils.isTelNumber(uri)) "tel:$uri" else uri,
- !blindChecked.value
- )
- showSelectItemDialog.value = false
- }
- showSelectItemDialog.value = true
- }
- else {
- if (uris.size == 1) uriText = uris[0]
- transfer(
- ua,
- if (Utils.isTelNumber(uriText)) "tel:$uriText" else uriText,
- !blindChecked.value
- )
- }
- keyboardController?.hide()
- showDialog.value = false
- showTransferDialog = false
- }
- },
- modifier = Modifier.padding(end = 16.dp),
- ) {
- Text(
- text = stringResource(
- if (blindChecked.value)
- R.string.transfer
- else
- R.string.call
- ).uppercase(),
- color = LocalCustomColors.current.primary
- )
- }
- }
- }
- }
- }
- }
-
- val focusRequester = remember { FocusRequester() }
- val shouldRequestFocus by focusDtmf
- TextField(
- value = dtmfText.value,
- onValueChange = {
- if (it.length > dtmfText.value.length) {
- val char = it.last()
- if (char.isDigit() || char == '*' || char == '#') {
- Log.d(TAG, "Got DTMF digit '$char'")
- ua.currentCall()?.sendDigit(char)
- }
- }
- dtmfText.value = it
- },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
- modifier = Modifier
- .width(80.dp)
- .focusRequester(focusRequester),
- enabled = dtmfEnabled.value,
- textStyle = TextStyle(fontSize = 16.sp),
- label = { LabelText(stringResource(R.string.dtmf)) },
- singleLine = true
- )
- LaunchedEffect(shouldRequestFocus) {
- if (shouldRequestFocus) {
- focusRequester.requestFocus()
- focusDtmf.value = false
- }
- }
-
- IconButton(
- modifier = Modifier.size(48.dp),
- onClick = {
- val call = ua.currentCall()
- val stats = call?.stats("audio")
- if (stats != null && call.startTime != null && stats != "") {
- val parts = stats.split(",") as java.util.ArrayList
- if (parts[2] == "0/0") {
- parts[2] = "?/?"
- parts[3] = "?/?"
- parts[4] = "?/?"
- }
- val codecs = call.audioCodecs()
- val duration = call.duration()
- val txCodec = codecs.split(',')[0].split("/")
- val rxCodec = codecs.split(',')[1].split("/")
- alertTitle.value = getString(R.string.call_info)
- alertMessage.value =
- "${String.format(getString(R.string.duration), duration)}\n" +
- "${getString(R.string.codecs)}: ${txCodec[0]} ch ${txCodec[2]}/" +
- "${rxCodec[0]} ch ${rxCodec[2]}\n" +
- "${String.format(getString(R.string.rate), parts[0])}\n" +
- "${
- String.format(
- getString(R.string.average_rate),
- parts[1]
- )
- }\n" +
- "${getString(R.string.packets)}: ${parts[2]}\n" +
- "${getString(R.string.lost)}: ${parts[3]}\n" +
- String.format(getString(R.string.jitter), parts[4])
- showAlert.value = true
- } else {
- alertTitle.value = getString(R.string.call_info)
- alertMessage.value = getString(R.string.call_info_not_available)
- showAlert.value = true
- }
- },
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(id = R.drawable.info),
- modifier = Modifier.size(36.dp),
- tint = Color.Unspecified,
- contentDescription = null,
- )
- }
- }
-
- if (showAnswerRejectButtons.value) {
-
- IconButton(
- modifier = Modifier.size(48.dp),
- onClick = {
- answer(ctx)
- },
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(id = R.drawable.call),
- modifier = Modifier.size(48.dp),
- tint = Color.Unspecified,
- contentDescription = null,
- )
- }
-
- Spacer(Modifier.weight(1f))
-
- IconButton(
- modifier = Modifier.size(48.dp),
- onClick = {
- reject()
- },
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(id = R.drawable.hangup),
- modifier = Modifier.size(48.dp),
- tint = Color.Unspecified,
- contentDescription = null,
- )
- }
- }
- }
-
- }
-
- private fun callClick(ctx: Context) {
- if (viewModel.selectedAor.value != "") {
- if (Utils.checkPermissions(ctx, arrayOf(RECORD_AUDIO))) {
- if (Call.inCall())
- return
- val uriText = callUri.value.trim()
- if (uriText.isNotEmpty()) {
- val uris = Contact.contactUris(uriText)
- if (uris.isEmpty())
- makeCall(uriText)
- else if (uris.size == 1)
- makeCall(uris[0])
- else {
- items.value = uris
- itemAction.value = { index ->
- makeCall(uris[index])
- }
- showSelectItemDialog.value = true
- }
- }
- else {
- val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
- val latestPeerUri = CallHistoryNew.aorLatestPeerUri(ua.account.aor)
- if (latestPeerUri != null)
- callUri.value = Utils.friendlyUri(this, latestPeerUri, ua.account)
- }
- }
- else
- Toast.makeText(applicationContext, R.string.no_calls, Toast.LENGTH_SHORT).show()
- }
- }
-
- private fun makeCall(uriText: String) {
- val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
- val aor = ua.account.aor
- val peerUri = if (Utils.isTelNumber(uriText))
- "tel:$uriText"
- else
- uriText
- val uri = if (Utils.isTelUri(peerUri)) {
- if (ua.account.telProvider == "") {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.no_telephony_provider), aor)
- showAlert.value = true
- return
- }
- Utils.telToSip(peerUri, ua.account)
- }
- else
- Utils.uriComplete(peerUri, aor)
- if (!Utils.checkUri(uri)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_sip_or_tel_uri), uri)
- showAlert.value = true
- }
- else if (!BaresipService.requestAudioFocus(applicationContext)) {
- Toast.makeText(applicationContext, R.string.audio_focus_denied,
- Toast.LENGTH_SHORT).show()
- } else {
- if (Build.VERSION.SDK_INT < 31) {
- Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
- am.mode = AudioManager.MODE_IN_COMMUNICATION
- runCall(ua, uri)
- } else {
- if (am.mode == AudioManager.MODE_IN_COMMUNICATION) {
- runCall(ua, uri)
- } else {
- audioModeChangedListener = AudioManager.OnModeChangedListener { mode ->
- if (mode == AudioManager.MODE_IN_COMMUNICATION) {
- Log.d(TAG, "Audio mode changed to MODE_IN_COMMUNICATION using " +
- "device ${am.communicationDevice!!.type}")
- if (audioModeChangedListener != null) {
- am.removeOnModeChangedListener(audioModeChangedListener!!)
- audioModeChangedListener = null
- }
- runCall(ua, uri)
- } else {
- Log.d(TAG, "Audio mode changed to mode ${am.mode} using " +
- "device ${am.communicationDevice!!.type}")
- }
- }
- am.addOnModeChangedListener(mainExecutor, audioModeChangedListener!!)
- Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
- am.mode = AudioManager.MODE_IN_COMMUNICATION
- }
- }
- }
- }
-
- private fun runCall(ua: UserAgent, uri: String) {
- callRunnable = Runnable {
- callRunnable = null
- if (!call(ua, uri)) {
- BaresipService.abandonAudioFocus(applicationContext)
- showCallButton.value = true
- showCancelButton.value = false
- }
- else {
- showCallButton.value = false
- showCancelButton.value = true
- }
- }
- callHandler.postDelayed(callRunnable!!, BaresipService.audioDelay)
- }
-
- private fun answer(ctx: Context) {
- val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
- val call = ua.currentCall()
- if (call != null) {
- Log.d(TAG, "AoR ${ua.account.aor} answering call from ${callUri.value}")
- val intent = Intent(ctx, BaresipService::class.java)
- intent.action = "Call Answer"
- intent.putExtra("uap", ua.uap)
- intent.putExtra("callp", call.callp)
- intent.putExtra("video", Api.VIDMODE_OFF)
- startService(intent)
- }
- }
-
- private fun reject() {
- val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
- val call = ua.currentCall()
- if (call != null) {
- val callp = call.callp
- Log.d(TAG, "AoR ${ua.account.aor} rejecting call $callp from ${callUri.value}")
- call.rejected = true
- Api.ua_hangup(ua.uap, callp, 486, "Busy Here")
- }
- }
-
- @Composable
- fun BottomBar(ctx: Context) {
- val buttonSize = 48.dp
- Row( modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 12.dp),
- horizontalArrangement = Arrangement.SpaceEvenly,
- verticalAlignment = Alignment.CenterVertically
- ) {
- if (showVmIcon)
- IconButton(
- onClick = {
- if (viewModel.selectedAor.value != "") {
- val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
- val acc = ua.account
- if (acc.vmUri != "") {
- dialogTitle.value = getString(R.string.voicemail_messages)
- dialogMessage.value = acc.vmMessages(ctx)
- positiveText.value = getString(R.string.listen)
- onPositiveClicked.value = {
- val i = Intent(ctx, MainActivity::class.java)
- i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
- Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
- i.putExtra("action", "call")
- i.putExtra("uap", ua.uap)
- i.putExtra("peer", acc.vmUri)
- startActivity(i)
- }
- negativeText.value = getString(R.string.cancel)
- onNegativeClicked.value = {}
- showDialog.value = true
- }
- }
- },
- modifier = Modifier
- .weight(1f)
- .size(buttonSize)
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(vmIcon.intValue),
- contentDescription = null,
- Modifier.size(buttonSize),
- tint = Color.Unspecified
- )
- }
-
- IconButton(
- onClick = {
- if (viewModel.selectedAor.value != "") {
- val i = Intent(ctx, ContactsActivity::class.java)
- val b = Bundle()
- b.putString("aor", viewModel.selectedAor.value)
- i.putExtras(b)
- contactsRequest.launch(i)
- }
- },
- modifier = Modifier
- .weight(1f)
- .size(buttonSize)
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(R.drawable.contacts),
- contentDescription = null,
- Modifier.size(buttonSize),
- tint = LocalCustomColors.current.secondary
- )
- }
-
- IconButton(
- onClick = {
- if (viewModel.selectedAor.value != "") {
- val i = Intent(this@MainActivity, ChatsActivity::class.java)
- val b = Bundle()
- b.putString("aor", viewModel.selectedAor.value)
- b.putString("peer", resumeUri)
- i.putExtras(b)
- chatRequests.launch(i)
- }
- },
- modifier = Modifier
- .weight(1f)
- .size(buttonSize)
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(messagesIcon.intValue),
- contentDescription = null,
- Modifier.size(buttonSize),
- tint = Color.Unspecified
- )
- }
-
- IconButton(
- onClick = {
- calls(ctx)
- },
- modifier = Modifier
- .weight(1f)
- .size(buttonSize)
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(R.drawable.calls),
- contentDescription = null,
- Modifier.size(buttonSize),
- tint = Color.Unspecified
- )
- }
-
- IconButton(
- onClick = { dialpad = !dialpad },
- modifier = Modifier
- .weight(1f)
- .size(buttonSize),
- enabled = dialpadButtonEnabled
- ) {
- Icon(
- imageVector = if (dialpad)
- ImageVector.vectorResource(R.drawable.dialpad_on)
- else
- ImageVector.vectorResource(R.drawable.dialpad_off),
- contentDescription = null,
- modifier = Modifier.size(buttonSize),
- tint = Color.Unspecified
- )
- }
- }
-
- }
-
- @Composable
- fun OnHoldNotice() {
- if (showOnHoldNotice.value)
- OutlinedButton(
- onClick = {},
- border = BorderStroke(1.dp, LocalCustomColors.current.accent),
- modifier = Modifier.padding(16.dp),
- shape = RoundedCornerShape(20)
- ) {
- Text(
- text = stringResource(R.string.call_is_on_hold),
- fontSize = 18.sp,
- color = LocalCustomColors.current.itemText,
- )
- }
- }
-
- @Composable
- fun AskPasswords(ctx: Context) {
- if (showPasswordsDialog.value) {
- if (passwordAccounts.isNotEmpty()) {
- val account = passwordAccounts.removeAt(0)
- val params = account.substringAfter(">")
- if (Utils.paramValue(params, "auth_user") != "" && Utils.paramValue(params, "auth_pass") == "") {
- val aor = account.substringAfter("<").substringBefore(">")
- PasswordDialog(
- ctx = ctx,
- showPasswordDialog = showPasswordsDialog,
- password = password,
- keyboardController = keyboardController,
- title = stringResource(R.string.authentication_password),
- message = stringResource(R.string.account) + " " + Utils.plainAor(aor),
- okAction = {
- if (password.value != "")
- BaresipService.aorPasswords[aor] = password.value
- showPasswordsDialog.value = true
- },
- cancelAction = {
- showPasswordsDialog.value = true
- }
- )
- }
- else {
- showPasswordsDialog.value = false
- showPasswordsDialog.value = true
- }
- }
- else
- requestPermissionsLauncher.launch(permissions)
- }
- }
-
- @Composable
- fun AskPassword(ctx: Context) {
- if (showPasswordDialog.value)
- PasswordDialog(
- ctx = ctx,
- showPasswordDialog = showPasswordDialog,
- password = password,
- keyboardController = keyboardController,
- title = passwordTitle,
- okAction = {
- if (password.value != "") {
- if (passwordTitle == getString(R.string.encrypt_password))
- backup(password.value)
- else
- restore(password.value)
- password.value = ""
- }
- },
- cancelAction = {
- if (downloadsOutputUri != null) {
- Utils.deleteFile(ctx, downloadsOutputUri!!)
- }
- }
- )
- }
-
- private fun calls(ctx: Context) {
- if (viewModel.selectedAor.value != "") {
- val i = Intent(ctx, CallsActivity::class.java)
- val b = Bundle()
- b.putString("aor", viewModel.selectedAor.value)
- i.putExtras(b)
- callsRequest.launch(i)
- }
- }
-
- private fun updateIcons(acc: Account?) {
- if (acc == null) {
- showVmIcon = false
- messagesIcon.intValue = R.drawable.messages
- callsIcon.intValue = R.drawable.calls
- }
- else {
- if (acc.vmUri != "") {
- showVmIcon = true
- vmIcon.intValue = if (acc.vmNew > 0)
- R.drawable.voicemail_new
- else
- R.drawable.voicemail
- } else
- showVmIcon = false
- messagesIcon.intValue= if (acc.unreadMessages)
- R.drawable.messages_unread
- else
- R.drawable.messages
- callsIcon.intValue = if (acc.missedCalls)
- R.drawable.calls_missed
- else
- R.drawable.calls
- }
- }
-
override fun onStart() {
super.onStart()
- Log.e(TAG, "Main onStart")
+ Log.i(TAG, "Main onStart")
val action = intent.getStringExtra("action")
if (action != null) {
// MainActivity was not visible when call, message, or transfer request came in
intent.removeExtra("action")
- handleIntent(intent, action)
+ handleIntent(applicationContext, viewModel, intent, action)
}
}
override fun onResume() {
super.onResume()
- Log.d(TAG, "Main onResume with action '$resumeAction'")
+ Log.d(TAG, "Main onResume")
nm.cancelAll()
- BaresipService.isMainVisible = true
- when (resumeAction) {
- "call show" -> {
- handleServiceEvent ("call incoming",
- arrayListOf(resumeCall!!.ua.uap, resumeCall!!.callp))
- }
- "call answer" -> {
- answer(this@MainActivity)
- showCall(resumeCall!!.ua)
- }
- "call missed" -> {
- calls(this@MainActivity)
- }
- "call reject" ->
- reject()
- "call" -> {
- callUri.value = Account.ofAor(viewModel.selectedAor.value)!!.resumeUri
- callClick(this@MainActivity)
- }
- "dial" -> {
- callUri.value = Account.ofAor(viewModel.selectedAor.value)!!.resumeUri
- }
- "call transfer", "transfer show", "transfer accept" ->
- handleServiceEvent("$resumeAction,$resumeUri",
- arrayListOf(resumeCall!!.ua.uap, resumeCall!!.callp))
- "message", "message show", "message reply" ->
- handleServiceEvent(resumeAction, arrayListOf(resumeUap, resumeUri))
- else -> {
- val incomingCall = Call.call("incoming")
- if (incomingCall != null) {
- spinToAor(incomingCall.ua.account.aor)
- } else {
- restoreActivities()
- if (uas.value.isNotEmpty()) {
- if (viewModel.selectedAor.value == "") {
- if (Call.inCall())
- spinToAor(Call.calls()[0].ua.account.aor)
- else
- spinToAor(uas.value.first().account.aor)
- }
- }
- }
- val ua = UserAgent.ofAor(viewModel.selectedAor.value)
- if (ua != null) {
- showCall(ua)
- updateIcons(ua.account)
- }
- }
- }
- resumeAction = ""
}
override fun onPause() {
super.onPause()
Log.d(TAG, "Main onPause")
- Utils.addActivity("main")
- BaresipService.isMainVisible = false
- callTimer?.stop()
- saveCallUri()
- }
-
- override fun onStop() {
- super.onStop()
- Log.d(TAG, "Main onStop")
}
override fun onDestroy() {
Log.d(TAG, "Main onDestroy")
- if (Build.VERSION.SDK_INT >= 33) {
- if (backInvokedCallback != null)
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
- }
- else
- onBackPressedCallback.remove()
-
this.unregisterReceiver(screenEventReceiver)
if (Build.VERSION.SDK_INT >= 31)
@@ -2176,17 +353,10 @@ class MainActivity : ComponentActivity() {
BaresipService.serviceEvent.removeObserver(serviceEventObserver)
BaresipService.serviceEvents.clear()
- BaresipService.activities.clear()
super.onDestroy()
}
- override fun onConfigurationChanged(newConfig: Configuration) {
- super.onConfigurationChanged(newConfig)
- if (dtmfEnabled.value)
- focusDtmf.value = true
- }
-
override fun onNewIntent(intent: Intent) {
// Called when MainActivity already exists at the top of current task
super.onNewIntent(intent)
@@ -2194,141 +364,26 @@ class MainActivity : ComponentActivity() {
this.setShowWhenLocked(true)
this.setTurnScreenOn(true)
- resumeAction = ""
- resumeUri = ""
-
Log.d(TAG, "onNewIntent with action/data '${intent.action}/${intent.data}'")
when (intent.action) {
ACTION_DIAL, ACTION_CALL, ACTION_VIEW ->
- callAction(intent.data, if (intent.action == ACTION_CALL) "call" else "dial")
+ callAction(
+ applicationContext,
+ viewModel,
+ intent.data,
+ if (intent.action == ACTION_CALL) "call" else "dial"
+ )
else -> {
val action = intent.getStringExtra("action")
if (action != null) {
intent.removeExtra("action")
- handleIntent(intent, action)
+ handleIntent(applicationContext, viewModel, intent, action)
}
}
}
}
- private fun callAction(uri: Uri?, action: String) {
- if (Call.inCall() || uas.value.isEmpty())
- return
- Log.d(TAG, "Action $action to $uri")
- if (uri != null) {
- when (uri.scheme) {
- "sip" -> {
- val uriStr = Utils.uriUnescape(uri.toString())
- var ua = UserAgent.ofDomain(Utils.uriHostPart(uriStr))
- if (ua == null && uas.value.isNotEmpty())
- ua = uas.value[0]
- if (ua == null) {
- Log.w(TAG, "No accounts for '$uriStr'")
- return
- }
- spinToAor(ua.account.aor)
- resumeAction = action
- ua.account.resumeUri = uriStr
- }
- "tel" -> {
- val uriStr = uri.toString().replace("%2B", "+")
- .replace("%20", "")
- .filterNot{setOf('-', ' ', '(', ')').contains(it)}
- var account: Account? = null
- for (a in Account.accounts())
- if (a.telProvider != "") {
- account = a
- break
- }
- if (account == null) {
- Log.w(TAG, "No telephony providers for '$uriStr'")
- return
- }
- spinToAor(account.aor)
- resumeAction = action
- account.resumeUri = uriStr
- }
- else -> {
- Log.w(TAG, "Unsupported URI scheme ${uri.scheme}")
- return
- }
- }
- }
- }
-
- private fun handleIntent(intent: Intent, action: String) {
- Log.d(TAG, "Handling intent '$action'")
- val ev = action.split(",")
- when (ev[0]) {
- "call", "dial" -> {
- if (Call.inCall()) {
- Toast.makeText(applicationContext, getString(R.string.call_already_active),
- Toast.LENGTH_SHORT).show()
- return
- }
- val uap = intent.getLongExtra("uap", 0L)
- val ua = UserAgent.ofUap(uap)
- if (ua == null) {
- Log.w(TAG, "handleIntent 'call' did not find ua $uap")
- return
- }
- spinToAor(ua.account.aor)
- resumeAction = action
- ua.account.resumeUri = intent.getStringExtra("peer")!!
- }
- "call show", "call answer" -> {
- val callp = intent.getLongExtra("callp", 0L)
- val call = Call.ofCallp(callp)
- if (call == null) {
- Log.w(TAG, "handleIntent '$action' did not find call $callp")
- return
- }
- val ua = call.ua
- spinToAor(ua.account.aor)
- resumeAction = action
- resumeCall = call
- }
- "call missed" -> {
- val uap = intent.getLongExtra("uap", 0L)
- val ua = UserAgent.ofUap(uap)
- if (ua == null) {
- Log.w(TAG, "handleIntent did not find ua $uap")
- return
- }
- spinToAor(ua.account.aor)
- resumeAction = action
- }
- "call transfer", "transfer show", "transfer accept" -> {
- val callp = intent.getLongExtra("callp", 0L)
- val call = Call.ofCallp(callp)
- if (call == null) {
- Log.w(TAG, "handleIntent '$action' did not find call $callp")
- moveTaskToBack(true)
- return
- }
- resumeAction = ev[0]
- resumeCall = call
- resumeUri = if (ev[0] == "call transfer")
- ev[1]
- else
- intent.getStringExtra("uri")!!
- }
- "message", "message show", "message reply" -> {
- val uap = intent.getLongExtra("uap", 0L)
- val ua = UserAgent.ofUap(uap)
- if (ua == null) {
- Log.w(TAG, "handleIntent did not find ua $uap")
- return
- }
- spinToAor(ua.account.aor)
- resumeAction = action
- resumeUap = uap
- resumeUri = intent.getStringExtra("peer")!!
- }
- }
- }
-
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val stream = if (am.mode == AudioManager.MODE_RINGTONE)
AudioManager.STREAM_RING
@@ -2348,315 +403,6 @@ class MainActivity : ComponentActivity() {
return super.onKeyDown(keyCode, event)
}
- private fun handleServiceEvent(event: String, params: ArrayList) {
-
- fun handleNextEvent(logMessage: String? = null) {
- if (logMessage != null)
- Log.w(TAG, logMessage)
- if (BaresipService.serviceEvents.isNotEmpty()) {
- val first = BaresipService.serviceEvents.removeAt(0)
- handleServiceEvent(first.event, first.params)
- }
- }
-
- if (taskId == -1) {
- handleNextEvent("Omit service event '$event' for task -1")
- return
- }
-
- if (event == "started") {
- val uriString = params[0] as String
- Log.d(TAG, "Handling service event 'started' with URI '$uriString'")
- if (!initialized) {
- // Android has restarted baresip when permission has been denied in app settings
- recreate()
- return
- }
- if (uriString != "")
- callAction(uriString.toUri(), "dial")
- else {
- if (viewModel.selectedAor.value == "" && uas.value.isNotEmpty())
- viewModel.updateSelectedAor(uas.value.first().account.aor)
- }
- if (Preferences(applicationContext).displayTheme != AppCompatDelegate.getDefaultNightMode()) {
- AppCompatDelegate.setDefaultNightMode(Preferences(applicationContext).displayTheme)
- }
- handleNextEvent()
- return
- }
-
- if (event == "stopped") {
- Log.d(TAG, "Handling service event 'stopped' with start error '${params[0]}'")
- if (params[0] != "") {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = getString(R.string.start_failed)
- showAlert.value = true
- }
- else {
- finishAndRemoveTask()
- if (restart)
- reStart()
- else
- exitProcess(0)
- }
- return
- }
-
- val uap = params[0] as Long
- val ua = UserAgent.ofUap(uap)
- if (ua == null) {
- handleNextEvent("handleServiceEvent '$event' did not find ua $uap")
- return
- }
-
- val ev = event.split(",")
- Log.d(TAG, "Handling service event '${ev[0]}' for $uap")
- val acc = ua.account
- val aor = ua.account.aor
-
- when (ev[0]) {
- "call rejected" -> {
- if (aor == viewModel.selectedAor.value)
- callsIcon.intValue = R.drawable.calls_missed
- }
- "call incoming", "call outgoing" -> {
- val callp = params[1] as Long
- if (BaresipService.isMainVisible) {
- spinToAor(aor)
- showCall(ua, Call.ofCallp(callp))
- } else {
- Log.d(TAG, "Reordering to front")
- BaresipService.activities.clear()
- BaresipService.serviceEvents.clear()
- val i = Intent(applicationContext, MainActivity::class.java)
- i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
- i.putExtra("action", "call show")
- i.putExtra("callp", callp)
- startActivity(i)
- return
- }
- }
- "call answered" -> {
- showCall(ua)
- }
- "call redirect" -> {
- val redirectUri = ev[1]
- val target = Utils.friendlyUri(this, redirectUri, acc)
- if (acc.autoRedirect) {
- redirect(ua, target)
- Toast.makeText(applicationContext,
- String.format(getString(R.string.redirect_notice), target),
- Toast.LENGTH_SHORT
- ).show()
- }
- else {
- dialogTitle.value = getString(R.string.redirect_request)
- dialogMessage.value = String.format(getString(R.string.redirect_request_query), target)
- positiveText.value = getString(R.string.yes)
- onPositiveClicked.value = {
- redirect(ua, target)
- }
- negativeText.value = getString(R.string.no)
- onNegativeClicked.value = {}
- showDialog.value = true
- }
- showCall(ua)
- }
- "call established" -> {
- if (aor == viewModel.selectedAor.value) {
- dtmfText.value = ""
- showCall(ua)
- }
- }
- "call update" -> {
- showCall(ua)
- }
- "call verify" -> {
- val callp = params[1] as Long
- val call = Call.ofCallp(callp)
- if (call == null) {
- handleNextEvent("Call $callp to be verified is not found")
- return
- }
- dialogTitle.value = getString(R.string.verify)
- dialogMessage.value = String.format(getString(R.string.verify_sas), ev[1])
- positiveText.value = getString(R.string.yes)
- onPositiveClicked.value = {
- call.security = if (Api.cmd_exec("zrtp_verify ${ev[2]}") != 0) {
- Log.e(TAG, "Command 'zrtp_verify ${ev[2]}' failed")
- R.drawable.locked_yellow
- } else {
- R.drawable.locked_green
- }
- call.zid = ev[2]
- if (aor == viewModel.selectedAor.value)
- securityIcon.intValue = call.security
- }
- negativeText.value = getString(R.string.no)
- onNegativeClicked.value = {
- call.security = R.drawable.locked_yellow
- call.zid = ev[2]
- if (aor == viewModel.selectedAor.value)
- securityIcon.intValue = R.drawable.locked_yellow
- onNegativeClicked.value = {}
- }
- showDialog.value = true
- }
- "call verified", "call secure" -> {
- val callp = params[1] as Long
- val call = Call.ofCallp(callp)
- if (call == null) {
- handleNextEvent("Call $callp that is verified is not found")
- return
- }
- if (aor == viewModel.selectedAor.value)
- securityIcon.intValue = call.security
- }
- "call transfer", "transfer show" -> {
- val callp = params[1] as Long
- if (!BaresipService.isMainVisible) {
- Log.d(TAG, "Reordering to front")
- BaresipService.activities.clear()
- BaresipService.serviceEvents.clear()
- val i = Intent(applicationContext, MainActivity::class.java)
- i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
- i.putExtra("action", event)
- i.putExtra("callp", callp)
- startActivity(i)
- return
- }
- val call = Call.ofCallp(callp)
- val target = Utils.friendlyUri(this, ev[1], acc)
- dialogTitle.value = if (call != null)
- getString(R.string.transfer_request)
- else
- getString(R.string.call_request)
- dialogMessage.value = if (call != null)
- String.format(getString(R.string.transfer_request_query), target)
- else
- String.format(getString(R.string.call_request_query), target)
- positiveText.value = getString(R.string.yes)
- onPositiveClicked.value = {
- if (call in Call.calls())
- acceptTransfer(ua, call!!, ev[1])
- else
- makeCall(ev[1])
- }
- negativeText.value = getString(R.string.no)
- onNegativeClicked.value = {
- if (call in Call.calls())
- call!!.notifySipfrag(603, "Decline")
- onNegativeClicked.value = {}
- }
- showDialog.value = true
- }
- "transfer accept" -> {
- val callp = params[1] as Long
- val call = Call.ofCallp(callp)
- if (call in Call.calls())
- Api.ua_hangup(uap, callp, 0, "")
- call(ua, ev[1])
- showCall(ua)
- }
- "transfer failed" -> {
- showCall(ua)
- }
- "call closed" -> {
- val call = ua.currentCall()
- if (call != null) {
- call.resume()
- startCallTimer(call)
- }
- else
- callTimer?.stop()
- if (aor == viewModel.selectedAor.value) {
- ua.account.resumeUri = ""
- showCall(ua)
- if (acc.missedCalls)
- callsIcon.intValue = R.drawable.calls_missed
- }
- if (kgm.isDeviceLocked)
- this.setShowWhenLocked(false)
- }
- "message", "message show", "message reply" -> {
- val i = Intent(applicationContext, ChatActivity::class.java)
- i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
- val b = Bundle()
- b.putString("aor", aor)
- b.putString("peer", params[1] as String)
- b.putBoolean("focus", ev[0] == "message reply")
- i.putExtras(b)
- chatRequests.launch(i)
- }
- "mwi notify" -> {
- val lines = ev[1].split("\n")
- for (line in lines) {
- if (line.startsWith("Voice-Message:")) {
- val counts = (line.split(" ")[1]).split("/")
- acc.vmNew = counts[0].toInt()
- acc.vmOld = counts[1].toInt()
- break
- }
- }
- if (aor == viewModel.selectedAor.value) {
- vmIcon.intValue = if (acc.vmNew > 0)
- R.drawable.voicemail_new
- else
- R.drawable.voicemail
- }
- }
- else -> Log.e(TAG, "Unknown event '${ev[0]}'")
- }
-
- handleNextEvent()
- }
-
- private fun redirect(ua: UserAgent, redirectUri: String) {
- if (ua.account.aor != viewModel.selectedAor.value)
- spinToAor(ua.account.aor)
- callUri.value = redirectUri
- callClick(this@MainActivity)
- }
-
- private fun reStart() {
- Log.d(TAG, "Trigger restart")
- val pm = applicationContext.packageManager
- val intent = pm.getLaunchIntentForPackage(this.packageName)
- this.startActivity(intent)
- exitProcess(0)
- }
-
- @RequiresApi(29)
- private fun pickupFileFromDownloads(action: String) {
- when (action) {
- "backup" -> {
- backupRequest.launch(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- type = "application/octet-stream"
- putExtra(Intent.EXTRA_TITLE, "baresip_" +
- SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.getDefault()).format(Date()))
- putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI)
- })
- }
- "restore" -> {
- restoreRequest.launch(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- type = "application/octet-stream"
- putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI)
- })
- }
- "logcat" -> {
- logcatRequest.launch(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- type = "text/plain"
- putExtra(Intent.EXTRA_TITLE, "baresip_logcat_" +
- SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.getDefault()).format(Date()))
- putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI)
- })
- }
- }
- }
-
private fun quitRestart(reStart: Boolean) {
Log.i(TAG, "quitRestart Restart = $reStart")
window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
@@ -2668,453 +414,18 @@ class MainActivity : ComponentActivity() {
} else {
finishAndRemoveTask()
if (reStart)
- reStart()
+ quitRestart(true)
else
exitProcess(0)
}
}
- private fun transfer(ua: UserAgent, uriText: String, attended: Boolean) {
- val uri = if (Utils.isTelUri(uriText))
- Utils.telToSip(uriText, ua.account)
- else
- Utils.uriComplete(uriText, ua.account.aor)
- if (!Utils.checkUri(uri)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = String.format(getString(R.string.invalid_sip_or_tel_uri), uri)
- showAlert.value = true
- }
- else {
- val call = ua.currentCall()
- if (call != null) {
- if (attended) {
- if (call.hold()) {
- call.referTo = uri
- call(ua, uri, call)
- }
- }
- else {
- if (!call.transfer(uri)) {
- alertTitle.value = getString(R.string.notice)
- alertMessage.value = getString(R.string.transfer_failed)
- showAlert.value = true
- }
- }
- showCall(ua)
- }
- }
- }
-
- private fun startBaresip() {
- if (!BaresipService.isStartReceived) {
- baresipService.action = "Start"
- startService(baresipService)
- if (atStartup)
- moveTaskToBack(true)
- }
- }
-
- private fun backup(password: String) {
- val files = arrayListOf("accounts", "config", "contacts", "call_history",
- "messages", "gzrtp.zid", "cert.pem", "ca_cert", "ca_certs.crt")
- File(BaresipService.filesPath).walk().forEach {
- if (it.name.endsWith(".png"))
- files.add(it.name)
- }
- val zipFile = getString(R.string.app_name) + ".zip"
- val zipFilePath = BaresipService.filesPath + "/$zipFile"
- if (!Utils.zip(files, zipFile)) {
- Log.w(TAG, "Failed to write zip file '$zipFile'")
- alertTitle.value = getString(R.string.error)
- alertMessage.value = String.format(getString(R.string.backup_failed),
- Utils.fileNameOfUri(applicationContext, downloadsOutputUri!!))
- showAlert.value = true
- downloadsOutputUri = null
- return
- }
- val content = Utils.getFileContents(zipFilePath)
- if (content == null) {
- Log.w(TAG, "Failed to read zip file '$zipFile'")
- alertTitle.value = getString(R.string.error)
- alertMessage.value = String.format(getString(R.string.backup_failed),
- Utils.fileNameOfUri(applicationContext, downloadsOutputUri!!))
- showAlert.value = true
- downloadsOutputUri = null
- return
- }
- if (!Utils.encryptToUri(applicationContext, downloadsOutputUri!!, content, password)) {
- alertTitle.value = getString(R.string.error)
- alertMessage.value = String.format(getString(R.string.backup_failed),
- Utils.fileNameOfUri(applicationContext, downloadsOutputUri!!))
- showAlert.value = true
- downloadsOutputUri = null
- return
- }
- alertTitle.value = getString(R.string.info)
- alertMessage.value = String.format(getString(R.string.backed_up),
- Utils.fileNameOfUri(applicationContext, downloadsOutputUri!!))
- showAlert.value = true
- Utils.deleteFile(File(zipFilePath))
- downloadsOutputUri = null
- }
-
- private fun restore(password: String) {
- val zipFile = getString(R.string.app_name) + ".zip"
- val zipFilePath = BaresipService.filesPath + "/$zipFile"
- val zipData = Utils.decryptFromUri(applicationContext, downloadsInputUri!!, password)
- if (zipData == null) {
- alertTitle.value = getString(R.string.error)
- alertMessage.value = String.format(getString(R.string.restore_failed),
- Utils.fileNameOfUri(applicationContext, downloadsOutputUri!!))
- showAlert.value = true
- downloadsOutputUri = null
- return
- }
- if (!Utils.putFileContents(zipFilePath, zipData)) {
- Log.w(TAG, "Failed to write zip file '$zipFile'")
- alertTitle.value = getString(R.string.error)
- alertMessage.value = String.format(getString(R.string.restore_failed),
- Utils.fileNameOfUri(applicationContext, downloadsOutputUri!!))
- showAlert.value = true
- downloadsOutputUri = null
- return
- }
- if (!Utils.unZip(zipFilePath)) {
- Log.w(TAG, "Failed to unzip file '$zipFile'")
- alertTitle.value = getString(R.string.error)
- alertMessage.value = String.format(getString(R.string.restore_unzip_failed), "baresip", BuildConfig.VERSION_NAME)
- showAlert.value = true
- downloadsOutputUri = null
- return
- }
- Utils.deleteFile(File(zipFilePath))
-
- File("${BaresipService.filesPath}/recordings").walk().forEach {
- if (it.name.startsWith("dump"))
- Utils.deleteFile(it)
- }
-
- dialogTitle.value = getString(R.string.info)
- dialogMessage.value = getString(R.string.restored)
- positiveText.value = getString(R.string.restart)
- onPositiveClicked.value = {
- quitRestart(true)
- }
- negativeText.value = getString(R.string.cancel)
- onNegativeClicked.value = {}
- showDialog.value = true
-
- downloadsOutputUri = null
- }
-
- private fun spinToAor(aor: String) {
- if (aor != viewModel.selectedAor.value)
- viewModel.updateSelectedAor(aor)
- updateIcons(Account.ofAor(aor))
- }
-
- private fun userAgentOfSelectedAor(): UserAgent {
- return UserAgent.ofAor(viewModel.selectedAor.value)!!
- }
-
- private fun call(ua: UserAgent, uri: String, onHoldCall: Call? = null): Boolean {
- spinToAor(ua.account.aor)
- val callp = ua.callAlloc(0L, Api.VIDMODE_OFF)
- return if (callp != 0L) {
- Log.d(TAG, "Adding outgoing call ${ua.uap}/$callp/$uri")
- val call = Call(callp, ua, uri, "out", "outgoing")
- call.onHoldCall = onHoldCall
- call.add()
- if (onHoldCall != null)
- onHoldCall.newCall = call
- if (call.connect(uri)) {
- showCall(ua)
- true
- } else {
- Log.w(TAG, "call_connect $callp failed")
- if (onHoldCall != null)
- onHoldCall.newCall = null
- call.remove()
- call.destroy()
- showCall(ua)
- false
- }
- } else {
- Log.w(TAG, "callAlloc for ${ua.uap} to $uri failed")
- false
- }
- }
-
- private fun acceptTransfer(ua: UserAgent, call: Call, uri: String) {
- val newCallp = ua.callAlloc(call.callp, Api.VIDMODE_OFF)
- if (newCallp != 0L) {
- Log.d(TAG, "Adding outgoing call ${ua.uap}/$newCallp/$uri")
- val newCall = Call(newCallp, ua, uri, "out", "transferring")
- newCall.add()
- if (newCall.connect(uri)) {
- if (ua.account.aor != viewModel.selectedAor.value)
- spinToAor(ua.account.aor)
- showCall(ua)
- } else {
- Log.w(TAG, "call_connect $newCallp failed")
- call.notifySipfrag(500, "Call Error")
- }
- } else {
- Log.w(TAG, "callAlloc for ua ${ua.uap} call ${call.callp} transfer failed")
- call.notifySipfrag(500, "Call Error")
- }
- }
-
- private fun showCall(ua: UserAgent?, showCall: Call? = null) {
- if (ua == null)
- return
- val call = showCall ?: ua.currentCall()
- if (call == null) {
- pullToRefreshEnabled = true
- if (ua.account.resumeUri != "")
- callUri.value = ua.account.resumeUri
- else
- callUri.value = ""
- callUriLabel.value = getString(R.string.outgoing_call_to_dots)
- callUriEnabled.value = true
- keyboardController?.hide()
- showCallTimer.value = false
- securityIcon.intValue = -1
- showHangupButton.value = false
- transferIcon.intValue = R.drawable.call_transfer
- dtmfEnabled.value = false
- focusDtmf.value = false
- showCallButton.value = true
- showCancelButton.value = false
- showAnswerRejectButtons.value = false
- showOnHoldNotice.value = false
- dialpadButtonEnabled = true
- if (BaresipService.isMicMuted) {
- BaresipService.isMicMuted = false
- micIcon = R.drawable.mic_on
- }
- } else {
- pullToRefreshEnabled = false
- callUriEnabled.value = false
- when (call.status) {
- "outgoing", "transferring", "answered" -> {
- callUriLabel.value = if (call.status == "answered")
- getString(R.string.incoming_call_from_dots)
- else
- getString(R.string.outgoing_call_to_dots)
- callUri.value = Utils.friendlyUri(this, call.peerUri, ua.account)
- showCallTimer.value = false
- securityIcon.intValue = -1
- showCallButton.value = false
- showCancelButton.value = call.status == "outgoing"
- showHangupButton.value = !showCancelButton.value
- showAnswerRejectButtons.value = false
- showOnHoldNotice.value = false
- dialpadButtonEnabled = false
- }
- "incoming" -> {
- showCallTimer.value = false
- securityIcon.intValue = -1
- val uri = call.diverterUri()
- if (uri != "") {
- callUriLabel.value = getString(R.string.diverted_by_dots)
- callUri.value = Utils.friendlyUri(this, uri, ua.account)
- }
- else {
- callUriLabel.value = getString(R.string.incoming_call_from_dots)
- callUri.value = Utils.friendlyUri(this, call.peerUri, ua.account)
- }
- showCallButton.value = false
- showCancelButton.value = false
- showHangupButton.value = false
- showAnswerRejectButtons.value = true
- showOnHoldNotice.value = false
- dialpadButtonEnabled = false
- }
- "connected" -> {
- if (call.referTo != "") {
- callUriLabel.value = getString(R.string.outgoing_call_to_dots)
- callUri.value = Utils.friendlyUri(this, call.referTo, ua.account)
- transferButtonEnabled.value = false
- } else {
- if (call.dir == "out") {
- callUriLabel.value = getString(R.string.outgoing_call_to_dots)
- callUri.value = Utils.friendlyUri(this, call.peerUri, ua.account)
- } else {
- callUriLabel.value = getString(R.string.incoming_call_from_dots)
- callUri.value = Utils.friendlyUri(this, call.peerUri, ua.account)
- }
- transferButtonEnabled.value = true
- }
- transferIcon.intValue = if (call.onHoldCall == null)
- R.drawable.call_transfer
- else
- R.drawable.call_transfer_execute
- showCallTimer.value = true
- startCallTimer(call)
- if (ua.account.mediaEnc == "")
- securityIcon.intValue = -1
- else
- securityIcon.intValue = call.security
- showCallButton.value = false
- showCancelButton.value = false
- showHangupButton.value = true
- showAnswerRejectButtons.value = false
- if (call.onhold)
- holdIcon.intValue = R.drawable.resume
- else
- holdIcon.intValue = R.drawable.call_hold
- Handler(Looper.getMainLooper()).postDelayed({
- showOnHoldNotice.value = call.held
- }, 100)
- if (call.held) {
- keyboardController?.hide()
- dtmfEnabled.value = false
- focusDtmf.value = false
- } else {
- dtmfEnabled.value = true
- focusDtmf.value = true
- if (resources.configuration.orientation == ORIENTATION_PORTRAIT)
- keyboardController?.show()
- }
- }
- }
- }
- }
-
- private fun restoreActivities() {
- if (BaresipService.activities.isEmpty()) return
- Log.d(TAG, "Activity stack ${BaresipService.activities}")
- val activity = BaresipService.activities[0].split(",")
- BaresipService.activities.removeAt(0)
- when (activity[0]) {
- "main" -> {
- if (!Call.inCall() && (BaresipService.activities.size > 1))
- restoreActivities()
- }
- "config" -> {
- configRequest.launch(Intent(this, ConfigActivity::class.java))
- }
- "audio" -> {
- startActivity(Intent(this, AudioActivity::class.java))
- }
- "accounts" -> {
- val i = Intent(this, AccountsActivity::class.java)
- val b = Bundle()
- b.putString("aor", activity[1])
- i.putExtras(b)
- accountsRequest.launch(i)
- }
- "account" -> {
- val i = Intent(this, AccountActivity::class.java)
- val b = Bundle()
- b.putString("aor", activity[1])
- i.putExtras(b)
- accountsRequest.launch(i)
- }
- "codecs" -> {
- val i = Intent(this, CodecsActivity::class.java)
- val b = Bundle()
- b.putString("aor", activity[1])
- b.putString("media", activity[2])
- i.putExtras(b)
- startActivity(i)
- }
- "about" -> {
- startActivity(Intent(this, AboutActivity::class.java))
- }
- "contacts" -> {
- val i = Intent(this, ContactsActivity::class.java)
- val b = Bundle()
- b.putString("aor", activity[1])
- i.putExtras(b)
- contactsRequest.launch(i)
- }
- "contact" -> {
- val i = Intent(this, BaresipContactActivity::class.java)
- val b = Bundle()
- if (activity[1] == "true") {
- b.putBoolean("new", true)
- b.putString("uri", activity[2])
- } else {
- b.putBoolean("new", false)
- b.putInt("index", activity[2].toInt())
- }
- i.putExtras(b)
- startActivity(i)
- }
- "chats" -> {
- val i = Intent(this, ChatsActivity::class.java)
- val b = Bundle()
- b.putString("aor", activity[1])
- i.putExtras(b)
- chatRequests.launch(i)
- }
- "chat" -> {
- val i = Intent(this, ChatActivity::class.java)
- val b = Bundle()
- b.putString("aor", activity[1])
- b.putString("peer", activity[2])
- b.putBoolean("focus", activity[3] == "true")
- i.putExtras(b)
- chatRequests.launch(i)
- }
- "calls" -> {
- val i = Intent(this, CallsActivity::class.java)
- val b = Bundle()
- b.putString("aor", activity[1])
- i.putExtras(b)
- callsRequest.launch(i)
- }
- "call_details" -> {
- val i = Intent(this, CallDetailsActivity::class.java)
- val b = Bundle()
- b.putString("aor", activity[1])
- b.putString("peer", activity[2])
- b.putInt("position", activity[3].toInt())
- i.putExtras(b)
- callsRequest.launch(i)
- }
- }
- return
- }
-
- private fun saveCallUri() {
- if (uas.value.isNotEmpty() && viewModel.selectedAor.value != "") {
- val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
- if (ua.calls().isEmpty())
- ua.account.resumeUri = callUri.value
- else
- ua.account.resumeUri = ""
- }
- }
-
- private fun startCallTimer(call: Call) {
- Handler(Looper.getMainLooper()).postDelayed({
- callTimer?.stop()
- callTimer?.base = SystemClock.elapsedRealtime() - (call.duration() * 1000L)
- callTimer?.start()
- }, 100)
- }
-
- private fun abandonAudioFocus() {
- if (Build.VERSION.SDK_INT < 31) {
- if (callRunnable != null) {
- callHandler.removeCallbacks(callRunnable!!)
- callRunnable = null
- BaresipService.abandonAudioFocus(applicationContext)
- }
- } else {
- if (audioModeChangedListener != null) {
- am.removeOnModeChangedListener(audioModeChangedListener!!)
- audioModeChangedListener = null
- BaresipService.abandonAudioFocus(applicationContext)
- }
- }
- }
-
- companion object {
- var activityAor = ""
+ private fun reStart() {
+ Log.d(TAG, "Trigger restart")
+ val pm = applicationContext.packageManager
+ val intent = pm.getLaunchIntentForPackage(this.packageName)
+ this.startActivity(intent)
+ exitProcess(0)
}
init {
diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt
new file mode 100644
index 00000000..6767833c
--- /dev/null
+++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt
@@ -0,0 +1,2617 @@
+package com.tutpro.baresip
+
+import android.Manifest.permission.READ_EXTERNAL_STORAGE
+import android.Manifest.permission.RECORD_AUDIO
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+import android.app.Activity.RESULT_OK
+import android.content.Context
+import android.content.Intent
+import android.content.res.Configuration.ORIENTATION_PORTRAIT
+import android.media.AudioManager
+import android.net.Uri
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.os.Process
+import android.os.SystemClock
+import android.provider.DocumentsContract
+import android.provider.MediaStore
+import android.widget.Chronometer
+import android.widget.Toast
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.gestures.detectHorizontalDragGestures
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.outlined.Clear
+import androidx.compose.material3.BasicAlertDialog
+import androidx.compose.material3.ButtonColors
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator
+import androidx.compose.material3.pulltorefresh.pullToRefresh
+import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.SoftwareKeyboardController
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.content.ContextCompat
+import androidx.core.net.toUri
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.currentBackStackEntryAsState
+import com.tutpro.baresip.BaresipService.Companion.contactNames
+import com.tutpro.baresip.BaresipService.Companion.uas
+import com.tutpro.baresip.BaresipService.Companion.uasStatus
+import com.tutpro.baresip.CustomElements.AlertDialog
+import com.tutpro.baresip.CustomElements.DropdownMenu
+import com.tutpro.baresip.CustomElements.LabelText
+import com.tutpro.baresip.CustomElements.PasswordDialog
+import com.tutpro.baresip.CustomElements.SelectableAlertDialog
+import com.tutpro.baresip.CustomElements.verticalScrollbar
+import kotlinx.coroutines.delay
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+private var dialpadButtonEnabled by mutableStateOf(true)
+private var pullToRefreshEnabled by mutableStateOf(true)
+
+private var downloadsInputUri: Uri? = null
+private var downloadsOutputUri: Uri? = null
+
+private val passwordTitle = mutableStateOf("")
+private val showPasswordDialog = mutableStateOf(false)
+private val showPasswordsDialog = mutableStateOf(false)
+
+private var passwordAccounts = mutableListOf()
+private var password = mutableStateOf("")
+
+private val selectItems = mutableStateOf(listOf())
+private val selectItemAction = mutableStateOf<(Int) -> Unit>({ _ -> run {} })
+private val showSelectItemDialog = mutableStateOf(false)
+
+private var keyboardController: SoftwareKeyboardController? = null
+private var callRunnable: Runnable? = null
+private var callHandler: Handler = Handler(Looper.getMainLooper())
+private var audioModeChangedListener: AudioManager.OnModeChangedListener? = null
+
+fun NavGraphBuilder.mainScreenRoute(
+ navController: NavController,
+ viewModel: ViewModel,
+ onRequestPermissions: () -> Unit,
+ onRestartApp: () -> Unit,
+ onQuitApp: () -> Unit
+) {
+ composable("main") {
+ MainScreen(
+ navController = navController,
+ viewModel = viewModel,
+ onRequestPermissions = onRequestPermissions,
+ onRestartClick = onRestartApp,
+ onQuitClick = onQuitApp
+ )
+ }
+}
+
+@Composable
+private fun MainScreen(
+ navController: NavController,
+ viewModel: ViewModel,
+ onRequestPermissions: () -> Unit,
+ onRestartClick: () -> Unit,
+ onQuitClick: () -> Unit
+) {
+ val ctx = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ DisposableEffect(lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_RESUME -> {
+ Log.d(TAG, "Resumed to MainScreen")
+ BaresipService.isMainVisible = true
+ val incomingCall = Call.call("incoming")
+ if (incomingCall != null)
+ spinToAor(viewModel, incomingCall.ua.account.aor)
+ else {
+ if (uas.value.isNotEmpty()) {
+ if (viewModel.selectedAor.value == "") {
+ if (Call.inCall())
+ spinToAor(viewModel, Call.calls()[0].ua.account.aor)
+ else
+ spinToAor(viewModel, uas.value.first().account.aor)
+ }
+ }
+ }
+ val ua = UserAgent.ofAor(viewModel.selectedAor.value)
+ if (ua != null) {
+ showCall(ctx, viewModel, ua)
+ updateIcons(viewModel, ua.account)
+ }
+ }
+ Lifecycle.Event.ON_PAUSE -> {
+ Log.d(TAG, "Paused from MainScreen")
+ BaresipService.isMainVisible = false
+ }
+ else -> {}
+ }
+ }
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ Log.d(TAG, "onDispose for MainScreen")
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ BaresipService.isMainVisible = false
+ }
+ }
+
+ val backupRequestLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == RESULT_OK) {
+ result.data?.data?.also { uri ->
+ downloadsOutputUri = uri
+ passwordTitle.value = ctx.getString(R.string.encrypt_password)
+ showPasswordDialog.value = true
+ }
+ }
+ }
+
+ @RequiresApi(29)
+ fun launchBackupRequest() {
+ if (Build.VERSION.SDK_INT < 29) {
+ if (!Utils.checkPermissions(ctx, arrayOf(WRITE_EXTERNAL_STORAGE))) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = ctx.getString(R.string.no_backup)
+ showAlert.value = true
+ }
+ else {
+ val path = Utils.downloadsPath("baresip.bs")
+ downloadsOutputUri = File(path).toUri()
+ passwordTitle.value = ctx.getString(R.string.encrypt_password)
+ showPasswordDialog.value = true
+ }
+ }
+ else {
+ val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "application/octet-stream"
+ putExtra(
+ Intent.EXTRA_TITLE,
+ "baresip_" + SimpleDateFormat(
+ "yyyy_MM_dd_HH_mm_ss",
+ Locale.getDefault()
+ ).format(Date())
+ )
+ putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI)
+ }
+ backupRequestLauncher.launch(intent)
+ }
+ }
+
+ val restoreRequestLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == RESULT_OK) {
+ result.data?.data?.also { uri ->
+ downloadsInputUri = uri
+ passwordTitle.value = ctx.getString(R.string.decrypt_password)
+ showPasswordDialog.value = true
+ }
+ }
+ }
+
+ @RequiresApi(29)
+ fun launchRestoreRequest() {
+ if (Build.VERSION.SDK_INT < 29) {
+ if (!Utils.checkPermissions(ctx, arrayOf(READ_EXTERNAL_STORAGE))) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = ctx.getString(R.string.no_restore)
+ showAlert.value = true
+ }
+ else {
+ val path = Utils.downloadsPath("baresip.bs")
+ downloadsInputUri = File(path).toUri()
+ passwordTitle.value = ctx.getString(R.string.decrypt_password)
+ showPasswordDialog.value = true
+ }
+ }
+ else {
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "application/octet-stream"
+ putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI)
+ }
+ restoreRequestLauncher.launch(intent)
+ }
+ }
+
+ val logcatRequestLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == RESULT_OK)
+ result.data?.data?.also { uri ->
+ try {
+ val out = ctx.contentResolver.openOutputStream(uri)
+ val process = Runtime.getRuntime().exec("logcat -d --pid=${Process.myPid()}")
+ val bufferedReader = process.inputStream.bufferedReader()
+ bufferedReader.forEachLine { line ->
+ out!!.write(line.toByteArray())
+ out.write('\n'.code.toByte().toInt())
+ }
+ out!!.close()
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to write logcat to file: $e")
+ }
+ }
+ }
+
+ fun launchLogcatRequest() {
+ if (Build.VERSION.SDK_INT >= 29) {
+ val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "text/plain"
+ putExtra(
+ Intent.EXTRA_TITLE,
+ "baresip_logcat_" + SimpleDateFormat(
+ "yyyy_MM_dd_HH_mm_ss",
+ Locale.getDefault()
+ ).format(Date())
+ )
+ putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI)
+ }
+ logcatRequestLauncher.launch(intent)
+ }
+ }
+
+ if (showPasswordDialog.value)
+ PasswordDialog(
+ ctx = ctx,
+ showPasswordDialog = showPasswordDialog,
+ password = password,
+ keyboardController = keyboardController,
+ title = passwordTitle.value,
+ okAction = {
+ if (password.value != "") {
+ if (passwordTitle.value == ctx.getString(R.string.encrypt_password))
+ backup(ctx, password.value)
+ else
+ restore(ctx, password.value, onRestartClick)
+ password.value = ""
+ }
+ },
+ cancelAction = {
+ if (downloadsOutputUri != null) {
+ Utils.deleteFile(ctx, downloadsOutputUri!!)
+ }
+ }
+ )
+
+ if (showPasswordsDialog.value) {
+ if (passwordAccounts.isNotEmpty()) {
+ val account = passwordAccounts.removeAt(0)
+ val params = account.substringAfter(">")
+ if (Utils.paramValue(params, "auth_user") != "" &&
+ Utils.paramValue(params, "auth_pass") == "") {
+ val aor = account.substringAfter("<").substringBefore(">")
+ PasswordDialog(
+ ctx = ctx,
+ showPasswordDialog = showPasswordsDialog,
+ password = password,
+ keyboardController = keyboardController,
+ title = stringResource(R.string.authentication_password),
+ message = stringResource(R.string.account) + " " + Utils.plainAor(aor),
+ okAction = {
+ if (password.value != "")
+ BaresipService.aorPasswords[aor] = password.value
+ showPasswordsDialog.value = true
+ },
+ cancelAction = {
+ showPasswordsDialog.value = true
+ }
+ )
+ } else {
+ showPasswordsDialog.value = false
+ showPasswordsDialog.value = true
+ }
+ }
+ else
+ onRequestPermissions()
+ }
+
+ LaunchedEffect(Unit) {
+ if (!BaresipService.isServiceRunning) {
+ val path = ctx.filesDir.absolutePath + "/accounts"
+ if (File(path).exists()) {
+ passwordAccounts = String(
+ Utils.getFileContents(path)!!,
+ Charsets.UTF_8
+ ).lines().toMutableList()
+ showPasswordsDialog.value = true
+ } else {
+ // Baresip is started for the first time
+ onRequestPermissions()
+ }
+ }
+ }
+
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+ val currentRoute = navBackStackEntry?.destination?.route
+
+ LaunchedEffect(currentRoute, viewModel.selectedAor.collectAsState()) {
+ if (currentRoute == "main") {
+ Log.d(TAG, "Updating icons for AOR: ${viewModel.selectedAor.value}")
+ val account = Account.ofAor(viewModel.selectedAor.value)
+ updateIcons(viewModel, account)
+ }
+ }
+
+ Scaffold(
+ modifier = Modifier
+ .fillMaxSize()
+ .imePadding(),
+ containerColor = LocalCustomColors.current.background,
+ topBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(
+ top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
+ )
+ ) {
+ TopAppBar(
+ viewModel = viewModel,
+ navController = navController,
+ onBackupClick = { launchBackupRequest() },
+ onRestoreClick = { launchRestoreRequest() },
+ onLogcatClick = { launchLogcatRequest() },
+ onRestartClick = onRestartClick,
+ onQuitClick = onQuitClick
+ )
+ }
+ },
+ bottomBar = { BottomBar(ctx, viewModel, navController) },
+ content = { contentPadding ->
+ MainContent(navController, viewModel, contentPadding)
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TopAppBar(
+ viewModel: ViewModel,
+ navController: NavController,
+ onBackupClick: () -> Unit,
+ onRestoreClick: () -> Unit,
+ onLogcatClick: () -> Unit,
+ onRestartClick: () -> Unit,
+ onQuitClick: () -> Unit
+) {
+ val ctx = LocalContext.current
+ val currentSpeakerIcon by viewModel.speakerIcon.collectAsState()
+ val currentMicIcon by viewModel.micIcon.collectAsState()
+
+ val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+
+ val recOffImage = ImageVector.vectorResource(R.drawable.rec_off)
+ val recOnImage = ImageVector.vectorResource(R.drawable.rec_on)
+ var recImage by remember { mutableStateOf(recOffImage) }
+
+ var menuExpanded by remember { mutableStateOf(false) }
+
+ val about = stringResource(R.string.about)
+ val settings = stringResource(R.string.configuration)
+ val accounts = stringResource(R.string.accounts)
+ val backup = stringResource(R.string.backup)
+ val restore = stringResource(R.string.restore)
+ val logcat = stringResource(R.string.logcat)
+ val restart = stringResource(R.string.restart)
+ val quit = stringResource(R.string.quit)
+
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(R.string.baresip),
+ color = LocalCustomColors.current.light,
+ fontSize = 22.sp,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ colors = TopAppBarDefaults.mediumTopAppBarColors(
+ containerColor = LocalCustomColors.current.primary
+ ),
+ windowInsets = WindowInsets(0, 0, 0, 0),
+ actions = {
+
+ Icon(
+ imageVector = recImage,
+ modifier = Modifier
+ .size(40.dp)
+ .combinedClickable(
+ onClick = {
+ if (Call.call("connected") == null) {
+ BaresipService.isRecOn = !BaresipService.isRecOn
+ recImage = if (BaresipService.isRecOn) {
+ Api.module_load("sndfile")
+ recOnImage
+ } else {
+ Api.module_unload("sndfile")
+ recOffImage
+ }
+ } else
+ Toast.makeText(ctx, R.string.rec_in_call, Toast.LENGTH_SHORT)
+ .show()
+ },
+ onLongClick = {
+ alertTitle.value = ctx.getString(R.string.call_recording_title)
+ alertMessage.value = ctx.getString(R.string.call_recording_tip)
+ showAlert.value = true
+ }
+ ),
+ tint = Color.Unspecified,
+ contentDescription = null
+ )
+
+ Spacer(modifier = Modifier.width(22.dp))
+
+ Icon(
+ imageVector = ImageVector.vectorResource(currentMicIcon),
+ modifier = Modifier
+ .size(40.dp)
+ .combinedClickable(
+ onClick = {
+ if (Call.call("connected") != null) {
+ BaresipService.isMicMuted = !BaresipService.isMicMuted
+ if (BaresipService.isMicMuted) {
+ viewModel.updateMicIcon(R.drawable.mic_off)
+ Api.calls_mute(true)
+ } else {
+ viewModel.updateMicIcon(R.drawable.mic_on)
+ Api.calls_mute(false)
+ }
+ }
+ },
+ onLongClick = {
+ alertTitle.value = ctx.getString(R.string.microphone_title)
+ alertMessage.value = ctx.getString(R.string.microphone_tip)
+ showAlert.value = true
+ },
+ ),
+ tint = Color.Unspecified,
+ contentDescription = null
+ )
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Icon(
+ imageVector = ImageVector.vectorResource(currentSpeakerIcon),
+ modifier = Modifier
+ .size(40.dp)
+ .combinedClickable(
+ onClick = {
+ if (Build.VERSION.SDK_INT >= 31)
+ Log.d(
+ TAG, "Toggling speakerphone when dev/mode is " +
+ "${am.communicationDevice!!.type}/${am.mode}"
+ )
+ Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am)
+ viewModel.updateSpeakerIcon(
+ if (Utils.isSpeakerPhoneOn(am))
+ R.drawable.speaker_on
+ else
+ R.drawable.speaker_off
+ )
+ },
+ onLongClick = {
+ alertTitle.value = ctx.getString(R.string.speakerphone_title)
+ alertMessage.value = ctx.getString(R.string.speakerphone_tip)
+ showAlert.value = true
+ },
+ ),
+ tint = Color.Unspecified,
+ contentDescription = null
+ )
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ IconButton(
+ onClick = { menuExpanded = !menuExpanded }
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Menu,
+ contentDescription = "Menu",
+ tint = LocalCustomColors.current.light
+ )
+ }
+
+ DropdownMenu(
+ expanded = menuExpanded,
+ onDismissRequest = { menuExpanded = false },
+ items = if (Build.VERSION.SDK_INT >= 29)
+ listOf(about, settings, accounts, backup, restore, logcat, restart, quit)
+ else
+ listOf(about, settings, accounts, backup, restore, restart, quit),
+ onItemClick = { selectedItem ->
+ menuExpanded = false
+ when (selectedItem) {
+ about -> { navController.navigate("about") }
+ settings -> { navController.navigate("settings") }
+ accounts -> { navController.navigate("accounts") }
+ backup -> onBackupClick()
+ restore -> onRestoreClick()
+ logcat -> onLogcatClick()
+ restart -> onRestartClick()
+ quit -> onQuitClick()
+ }
+ }
+ )
+ }
+ )
+}
+
+@Composable
+private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavController) {
+
+ val vmIcon by viewModel.vmIcon.collectAsState()
+ val showVmIcon by viewModel.showVmIcon.collectAsState()
+ val messagesIcon by viewModel.messagesIcon.collectAsState()
+ val callsIcon by viewModel.callsIcon.collectAsState()
+ val dialpadIcon by viewModel.dialpadIcon.collectAsState()
+
+ val buttonSize = 48.dp
+
+ Row( modifier = Modifier
+ .fillMaxWidth()
+ .navigationBarsPadding()
+ .padding(bottom = 16.dp),
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (showVmIcon)
+ IconButton(
+ onClick = {
+ if (viewModel.selectedAor.value != "") {
+ val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
+ val acc = ua.account
+ if (acc.vmUri != "") {
+ dialogTitle.value = ctx.getString(R.string.voicemail_messages)
+ dialogMessage.value = acc.vmMessages(ctx)
+ positiveText.value = ctx.getString(R.string.listen)
+ onPositiveClicked.value = {
+ val intent = Intent(ctx, MainActivity::class.java)
+ intent.putExtra("uap", ua.uap)
+ intent.putExtra("peer", acc.vmUri)
+ handleIntent(ctx, viewModel, intent, "call")
+ }
+ negativeText.value = ctx.getString(R.string.cancel)
+ onNegativeClicked.value = {}
+ showDialog.value = true
+ }
+ }
+ },
+ modifier = Modifier
+ .weight(1f)
+ .size(buttonSize)
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(vmIcon),
+ contentDescription = null,
+ Modifier.size(buttonSize),
+ tint = Color.Unspecified
+ )
+ }
+
+ IconButton(
+ onClick = { navController.navigate("contacts") },
+ modifier = Modifier
+ .weight(1f)
+ .size(buttonSize)
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(R.drawable.contacts),
+ contentDescription = null,
+ Modifier.size(buttonSize),
+ tint = LocalCustomColors.current.secondary
+ )
+ }
+
+ IconButton(
+ onClick = {
+ if (viewModel.selectedAor.value != "")
+ navController.navigate("chats/${viewModel.selectedAor.value}")
+ },
+ modifier = Modifier
+ .weight(1f)
+ .size(buttonSize)
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(messagesIcon),
+ contentDescription = null,
+ Modifier.size(buttonSize),
+ tint = Color.Unspecified
+ )
+ }
+
+ IconButton(
+ onClick = {
+ if (viewModel.selectedAor.value != "")
+ navController.navigate("calls/${viewModel.selectedAor.value}")
+ },
+ modifier = Modifier
+ .weight(1f)
+ .size(buttonSize)
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(callsIcon),
+ contentDescription = null,
+ Modifier.size(buttonSize),
+ tint = Color.Unspecified
+ )
+ }
+
+ IconButton(
+ onClick = { viewModel.updateDialpadIcon(
+ if (dialpadIcon == R.drawable.dialpad_off)
+ R.drawable.dialpad_on
+ else
+ R.drawable.dialpad_off
+ ) },
+ modifier = Modifier
+ .weight(1f)
+ .size(buttonSize),
+ enabled = dialpadButtonEnabled
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(dialpadIcon),
+ contentDescription = null,
+ modifier = Modifier.size(buttonSize),
+ tint = Color.Unspecified
+ )
+ }
+ }
+
+}
+
+private val callUri = mutableStateOf("")
+private var callUriEnabled = mutableStateOf(true)
+private val callUriLabel = mutableStateOf("")
+private var securityIcon = mutableIntStateOf(-1)
+private val showCallTimer = mutableStateOf(false)
+private var callDuration = 0
+private val showSuggestions = mutableStateOf(false)
+private val showCallButton = mutableStateOf(true)
+private val showCancelButton = mutableStateOf(false)
+private val showAnswerRejectButtons = mutableStateOf(false)
+private val showHangupButton = mutableStateOf(false)
+private val showOnHoldNotice = mutableStateOf(false)
+private var holdIcon = mutableIntStateOf(R.drawable.call_hold)
+private var transferButtonEnabled = mutableStateOf(false)
+private val transferIcon = mutableIntStateOf(R.drawable.call_transfer)
+private var dtmfText = mutableStateOf("")
+private val dtmfEnabled = mutableStateOf(false)
+private var focusDtmf = mutableStateOf(false)
+
+private val alertTitle = mutableStateOf("")
+private val alertMessage = mutableStateOf("")
+private val showAlert = mutableStateOf(false)
+
+private val dialogTitle = mutableStateOf("")
+private val dialogMessage = mutableStateOf("")
+private val positiveText = mutableStateOf("")
+private val onPositiveClicked = mutableStateOf({})
+private val negativeText = mutableStateOf("")
+private val onNegativeClicked = mutableStateOf({})
+private val showDialog = mutableStateOf(false)
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun MainContent(navController: NavController, viewModel: ViewModel, contentPadding: PaddingValues) {
+
+ var isRefreshing by remember { mutableStateOf(false) }
+ val refreshState = rememberPullToRefreshState()
+ var offset by remember { mutableFloatStateOf(0f) }
+ val swipeThreshold = 200
+ val ctx = LocalContext.current
+
+ LaunchedEffect(isRefreshing) {
+ if (isRefreshing) {
+ delay(1000)
+ isRefreshing = false
+ }
+ }
+
+ if (showAlert.value)
+ AlertDialog(
+ showDialog = showAlert,
+ title = alertTitle.value,
+ message = alertMessage.value,
+ positiveButtonText = stringResource(R.string.ok),
+ )
+
+ if (showDialog.value)
+ AlertDialog(
+ showDialog = showDialog,
+ title = stringResource(R.string.confirmation),
+ message = dialogMessage.value,
+ positiveButtonText = positiveText.value,
+ onPositiveClicked = onPositiveClicked.value,
+ negativeButtonText = negativeText.value,
+ onNegativeClicked = onNegativeClicked.value
+ )
+
+ SelectableAlertDialog(
+ openDialog = showSelectItemDialog,
+ title = stringResource(R.string.choose_destination_uri),
+ items = selectItems.value,
+ onItemClicked = selectItemAction.value,
+ neutralButtonText = stringResource(R.string.cancel),
+ onNeutralClicked = {}
+ )
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(contentPadding)
+ .padding(top = 18.dp, bottom = 6.dp, start = 16.dp, end = 16.dp)
+ .fillMaxSize()
+ .pullToRefresh(
+ state = refreshState,
+ isRefreshing = isRefreshing,
+ onRefresh = {
+ isRefreshing = true
+ if (uas.value.isNotEmpty()) {
+ if (viewModel.selectedAor.value == "")
+ spinToAor(viewModel, uas.value.first().account.aor)
+ val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
+ if (ua.account.regint > 0)
+ Api.ua_register(ua.uap)
+ }
+ },
+ enabled = pullToRefreshEnabled,
+ )
+ .pointerInput(Unit) {
+ detectHorizontalDragGestures(
+ onDragStart = { offset = 0f },
+ onDragEnd = {
+ if (offset < -swipeThreshold) {
+ if (uas.value.isNotEmpty()) {
+ val curPos = UserAgent.findAorIndex(viewModel.selectedAor.value)
+ val newPos = if (curPos == null)
+ 0
+ else
+ (curPos + 1) % uas.value.size
+ if (curPos != newPos) {
+ val ua = uas.value[newPos]
+ spinToAor(viewModel, ua.account.aor)
+ showCall(ctx, viewModel, ua)
+ }
+ }
+ } else if (offset > swipeThreshold) {
+ if (uas.value.isNotEmpty()) {
+ val curPos = UserAgent.findAorIndex(viewModel.selectedAor.value)
+ val newPos = when (curPos) {
+ null -> 0
+ 0 -> uas.value.size - 1
+ else -> curPos - 1
+ }
+ if (curPos != newPos) {
+ val ua = uas.value[newPos]
+ spinToAor(viewModel, ua.account.aor)
+ showCall(ctx, viewModel, ua)
+ }
+ }
+ }
+ }
+ ) { _, dragAmount ->
+ offset += dragAmount
+ }
+ }
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ AccountSpinner(ctx, viewModel, navController)
+ CallUriRow(ctx, viewModel)
+ CallRow(ctx, viewModel)
+ if (showOnHoldNotice.value)
+ OnHoldNotice()
+ Indicator(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ isRefreshing = isRefreshing,
+ state = refreshState,
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun AccountSpinner(ctx: Context, viewModel: ViewModel, navController: NavController) {
+
+ var expanded by rememberSaveable { mutableStateOf(false) }
+ val selected: String by viewModel.selectedAor.collectAsState()
+
+ if (uas.value.isEmpty())
+ viewModel.updateSelectedAor("")
+ else
+ if (selected == "" || UserAgent.ofAor(selected) == null) {
+ viewModel.updateSelectedAor(uas.value.first().account.aor)
+ }
+
+ showCall(ctx, viewModel, UserAgent.ofAor(selected))
+
+ updateIcons(viewModel, Account.ofAor(selected))
+
+ if (selected == "") {
+ OutlinedButton(
+ onClick = {
+ navController.navigate("accounts")
+ },
+ modifier = Modifier
+ .padding(horizontal = 4.dp)
+ .height(50.dp)
+ .fillMaxWidth(),
+ colors = ButtonColors(
+ containerColor = LocalCustomColors.current.grayLight,
+ contentColor = LocalCustomColors.current.dark,
+ disabledContainerColor = LocalCustomColors.current.grayLight,
+ disabledContentColor = LocalCustomColors.current.dark
+ ),
+ shape = RoundedCornerShape(12.dp),
+ contentPadding = PaddingValues(horizontal = 10.dp)
+ ) {
+ Text(text = "")
+ }
+ }
+ else
+ OutlinedButton(
+ onClick = {
+ expanded = !expanded
+ },
+ enabled = true,
+ modifier = Modifier
+ .padding(horizontal = 4.dp)
+ .height(50.dp)
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onPress = {
+ expanded = true
+ },
+ onLongPress = {
+ val ua = UserAgent.ofAor(selected)
+ if (ua != null) {
+ val acc = ua.account
+ if (Api.account_regint(acc.accp) > 0) {
+ Api.account_set_regint(acc.accp, 0)
+ Api.ua_unregister(ua.uap)
+ } else {
+ Api.account_set_regint(
+ acc.accp,
+ acc.configuredRegInt
+ )
+ Api.ua_register(ua.uap)
+ }
+ acc.regint = Api.account_regint(acc.accp)
+ Account.saveAccounts()
+ }
+ }
+ )
+ },
+ colors = ButtonColors(
+ containerColor = LocalCustomColors.current.grayLight,
+ contentColor = LocalCustomColors.current.dark,
+ disabledContainerColor = LocalCustomColors.current.grayLight,
+ disabledContentColor = LocalCustomColors.current.dark
+ ),
+ shape = RoundedCornerShape(12.dp),
+ contentPadding = PaddingValues(horizontal = 10.dp)
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(
+ uasStatus.value[selected] ?:
+ R.drawable.locked_yellow),
+ contentDescription = null,
+ tint = Color.Unspecified,
+ modifier = Modifier
+ .padding(end = 10.dp)
+ .clickable(onClick = {
+ navController.navigate("account/$selected/old")
+ })
+ )
+ Text(
+ text = Account.ofAor(selected)?.text() ?: "",
+ fontSize = 17.sp,
+ fontWeight = FontWeight.Bold,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ modifier = Modifier
+ .weight(1f)
+ .combinedClickable(
+ onClick = { expanded = true },
+ onLongClick = {
+ val ua = UserAgent.ofAor(selected)
+ if (ua != null) {
+ val acc = ua.account
+ if (Api.account_regint(acc.accp) > 0) {
+ Api.account_set_regint(acc.accp, 0)
+ Api.ua_unregister(ua.uap)
+ } else {
+ Api.account_set_regint(
+ acc.accp,
+ acc.configuredRegInt
+ )
+ Api.ua_register(ua.uap)
+ }
+ acc.regint = Api.account_regint(acc.accp)
+ Account.saveAccounts()
+ }
+ }
+ )
+ )
+ Icon(
+ imageVector = if (expanded)
+ Icons.Default.KeyboardArrowUp
+ else
+ Icons.Default.KeyboardArrowDown,
+ contentDescription = null
+ )
+ androidx.compose.material3.DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+ uas.value.forEachIndexed { _, ua ->
+ val acc = ua.account
+ DropdownMenuItem(
+ onClick = {
+ expanded = false
+ viewModel.updateSelectedAor(acc.aor)
+ showCall(ctx, viewModel, ua)
+ updateIcons(viewModel, acc)
+ },
+ text = { Text(
+ text = acc.text(),
+ fontSize = 17.sp,
+ fontWeight = FontWeight.Bold
+ ) },
+ leadingIcon = {
+ Icon(
+ imageVector = ImageVector.vectorResource(uasStatus.value[acc.aor]!!),
+ contentDescription = null,
+ tint = Color.Unspecified,
+ )
+ }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun CallUriRow(ctx: Context, viewModel: ViewModel) {
+
+ val suggestions by remember { contactNames }
+ var filteredSuggestions by remember { mutableStateOf(suggestions) }
+ val focusRequester = remember { FocusRequester() }
+ val lazyListState = rememberLazyListState()
+ val dialpadIcon by viewModel.dialpadIcon.collectAsState()
+
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 4.dp, bottom = 8.dp),
+ verticalAlignment = Alignment.CenterVertically) {
+ Column(
+ modifier = Modifier.weight(1f),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ OutlinedTextField(
+ value = callUri.value,
+ enabled = callUriEnabled.value,
+ singleLine = true,
+ colors = OutlinedTextFieldDefaults.colors(
+ disabledBorderColor = OutlinedTextFieldDefaults.colors().unfocusedIndicatorColor),
+ onValueChange = {
+ if (it != callUri.value) {
+ callUri.value = it
+ filteredSuggestions = suggestions.filter { suggestion ->
+ it.length > 2 && suggestion.startsWith(it, ignoreCase = true)
+ }
+ showSuggestions.value = it.length > 2
+ }
+ },
+ trailingIcon = {
+ if (callUriEnabled.value && callUri.value.isNotEmpty())
+ Icon(Icons.Outlined.Clear,
+ contentDescription = null,
+ modifier = Modifier
+ .clickable {
+ if (showSuggestions.value)
+ showSuggestions.value = false
+ else
+ callUri.value = ""
+ }
+ )
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 4.dp, end = 4.dp, top = 12.dp, bottom = 2.dp)
+ .focusRequester(focusRequester)
+ .onFocusChanged {
+ val account = Account.ofAor(viewModel.selectedAor.value)
+ if (account != null)
+ if (account.numericKeypad)
+ viewModel.updateDialpadIcon(R.drawable.dialpad_on)
+ },
+ label = {
+ LabelText(
+ text = callUriLabel.value,
+ fontSize = 18.sp,
+ )
+ },
+ textStyle = TextStyle(
+ fontSize = 18.sp,
+ color = LocalCustomColors.current.itemText
+ ),
+ keyboardOptions = if (dialpadIcon == R.drawable.dialpad_on)
+ KeyboardOptions(keyboardType = KeyboardType.Phone)
+ else
+ KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .shadow(8.dp, RoundedCornerShape(8.dp))
+ .background(
+ LocalCustomColors.current.grayLight,
+ shape = RoundedCornerShape(8.dp)
+ )
+ .animateContentSize()
+ ) {
+ if (showSuggestions.value && filteredSuggestions.isNotEmpty()) {
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(max = 150.dp)) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScrollbar(
+ state = lazyListState,
+ color = LocalCustomColors.current.gray
+ ),
+ horizontalAlignment = Alignment.Start,
+ state = lazyListState
+ ) {
+ items(
+ items = filteredSuggestions,
+ key = { suggestion -> suggestion }
+ ) { suggestion ->
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ callUri.value = suggestion
+ showSuggestions.value = false
+ }
+ .padding(12.dp)
+ ) {
+ Text(
+ text = suggestion,
+ modifier = Modifier.fillMaxWidth(),
+ color = LocalCustomColors.current.grayDark,
+ fontSize = 18.sp
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ if (showCallTimer.value) {
+ val textColor = LocalCustomColors.current.itemText.toArgb()
+ AndroidView(
+ factory = { context ->
+ val chronometer = Chronometer(context).apply {
+ textSize = 16F
+ setTextColor(textColor)
+ base = SystemClock.elapsedRealtime() - (callDuration * 1000L)
+ start()
+ }
+ chronometer
+ },
+ modifier = Modifier.padding(start = 6.dp,
+ top = 4.dp,
+ end = if (securityIcon.intValue != -1) 6.dp else 0.dp),
+ )
+ }
+ if (securityIcon.intValue != -1) {
+ Icon(
+ imageVector = ImageVector.vectorResource(securityIcon.intValue),
+ contentDescription = null,
+ modifier = Modifier
+ .size(28.dp)
+ .padding(top = 4.dp)
+ .clickable {
+ when (securityIcon.intValue) {
+ R.drawable.unlocked -> {
+ alertTitle.value = ctx.getString(R.string.alert)
+ alertMessage.value = ctx.getString(R.string.call_not_secure)
+ showAlert.value = true
+ }
+
+ R.drawable.locked_yellow -> {
+ alertTitle.value = ctx.getString(R.string.alert)
+ alertMessage.value = ctx.getString(R.string.peer_not_verified)
+ showAlert.value = true
+ }
+
+ R.drawable.locked_green -> {
+ dialogTitle.value = ctx.getString(R.string.info)
+ dialogMessage.value = ctx.getString(R.string.call_is_secure)
+ positiveText.value = ctx.getString(R.string.unverify)
+ onPositiveClicked.value = {
+ val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
+ val call = ua.currentCall()
+ if (call != null) {
+ if (Api.cmd_exec("zrtp_unverify " + call.zid) != 0)
+ Log.e(
+ TAG,
+ "Command 'zrtp_unverify ${call.zid}' failed"
+ )
+ else
+ securityIcon.intValue = R.drawable.locked_yellow
+ }
+ }
+ negativeText.value = ctx.getString(R.string.cancel)
+ showDialog.value = true
+ }
+ }
+ },
+ tint = Color.Unspecified,
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun CallRow(ctx: Context, viewModel: ViewModel) {
+
+ val dialpadIcon by viewModel.dialpadIcon.collectAsState()
+
+ Row( modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 6.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Absolute.SpaceBetween
+ ) {
+ if (showCallButton.value)
+ IconButton(
+ modifier = Modifier.size(48.dp),
+ onClick = {
+ showSuggestions.value = false
+ callClick(ctx, viewModel)
+ },
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.call),
+ modifier = Modifier.size(48.dp),
+ tint = Color.Unspecified,
+ contentDescription = null,
+ )
+ }
+
+ if (showCancelButton.value) {
+ Spacer(modifier = Modifier.weight(1f))
+ IconButton(
+ modifier = Modifier.size(48.dp),
+ onClick = {
+ showSuggestions.value = false
+ abandonAudioFocus(ctx)
+ var ua: UserAgent = UserAgent.ofAor(viewModel.selectedAor.value)!!
+ val call = ua.currentCall()
+ if (call != null) {
+ val callp = call.callp
+ Log.d(
+ TAG,
+ "AoR ${ua.account.aor} hanging up call $callp with ${callUri.value}"
+ )
+ Api.ua_hangup(ua.uap, callp, 0, "")
+ }
+ },
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.hangup),
+ modifier = Modifier.size(48.dp),
+ tint = Color.Unspecified,
+ contentDescription = null,
+ )
+ }
+ Spacer(modifier = Modifier.width(12.dp))
+ }
+
+ if (showHangupButton.value) {
+
+ IconButton(
+ modifier = Modifier.size(48.dp),
+ onClick = {
+ val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
+ abandonAudioFocus(ctx)
+ val uaCalls = ua.calls()
+ if (uaCalls.isNotEmpty()) {
+ val call = uaCalls.last()
+ val callp = call.callp
+ Log.d(TAG, "AoR ${ua.account.aor} hanging up call $callp with ${callUri.value}")
+ Api.ua_hangup(ua.uap, callp, 0, "")
+ }
+ }
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.hangup),
+ modifier = Modifier.size(48.dp),
+ tint = Color.Unspecified,
+ contentDescription = null,
+ )
+ }
+
+ IconButton(
+ modifier = Modifier.size(48.dp),
+ onClick = {
+ val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
+ val aor = ua.account.aor
+ val call = ua.currentCall()
+ if (call != null) {
+ if (call.onhold) {
+ Log.d(
+ TAG,
+ "AoR $aor resuming call ${call.callp} with ${callUri.value}"
+ )
+ call.resume()
+ call.onhold = false
+ holdIcon.intValue = R.drawable.call_hold
+ } else {
+ Log.d(
+ TAG,
+ "AoR $aor holding call ${call.callp} with ${callUri.value}"
+ )
+ call.hold()
+ call.onhold = true
+ holdIcon.intValue = R.drawable.resume
+ }
+ }
+ },
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = holdIcon.intValue),
+ modifier = Modifier.size(48.dp),
+ tint = Color.Unspecified,
+ contentDescription = null,
+ )
+ }
+
+ var showTransferDialog by remember { mutableStateOf(false) }
+ IconButton(
+ modifier = Modifier.size(48.dp),
+ enabled = transferButtonEnabled.value,
+ onClick = {
+ val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
+ val call = ua.currentCall()
+ if (call != null) {
+ if (call.onHoldCall != null) {
+ if (!call.executeTransfer()) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = ctx.getString(R.string.transfer_failed)
+ showAlert.value = true
+ }
+ } else
+ showTransferDialog = true
+ }
+ },
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(transferIcon.intValue),
+ modifier = Modifier.size(48.dp),
+ tint = Color.Unspecified,
+ contentDescription = null,
+ )
+ }
+
+ if (showTransferDialog) {
+
+ val showDialog = remember { mutableStateOf(true) }
+ val blindChecked = remember { mutableStateOf(true) }
+ val selectedAor: String by viewModel.selectedAor.collectAsState()
+ val ua = UserAgent.ofAor(selectedAor)!!
+ val call = ua.currentCall()
+
+ if (showDialog.value)
+ BasicAlertDialog(
+ onDismissRequest = {
+ keyboardController?.hide()
+ showDialog.value = false
+ showTransferDialog = false
+ }
+ ) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 0.dp),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = LocalCustomColors.current.cardBackground
+ )
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ text = stringResource(R.string.call_transfer),
+ fontSize = 20.sp,
+ color = LocalCustomColors.current.alert,
+ )
+ var transferUri by remember { mutableStateOf("") }
+ val suggestions by remember { contactNames }
+ var filteredSuggestions by remember { mutableStateOf(suggestions) }
+ val focusRequester = remember { FocusRequester() }
+ val lazyListState = rememberLazyListState()
+ OutlinedTextField(
+ value = transferUri,
+ singleLine = true,
+ onValueChange = {
+ if (it != transferUri) {
+ transferUri = it
+ filteredSuggestions =
+ suggestions.filter { suggestion ->
+ transferUri.length > 2 &&
+ suggestion.startsWith(
+ transferUri,
+ ignoreCase = true
+ )
+ }
+ showSuggestions.value = transferUri.length > 2
+ }
+ },
+ trailingIcon = {
+ if (transferUri.isNotEmpty())
+ Icon(
+ Icons.Outlined.Clear,
+ contentDescription = null,
+ modifier = Modifier.clickable {
+ if (showSuggestions.value)
+ showSuggestions.value = false
+ else
+ transferUri = ""
+ }
+ )
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ start = 4.dp,
+ end = 4.dp,
+ top = 12.dp,
+ bottom = 2.dp
+ )
+ .focusRequester(focusRequester),
+ label = { LabelText(stringResource(R.string.transfer_destination)) },
+ textStyle = TextStyle(
+ fontSize = 18.sp,
+ color = LocalCustomColors.current.itemText
+ ),
+ keyboardOptions = if (dialpadIcon == R.drawable.dialpad_on)
+ KeyboardOptions(keyboardType = KeyboardType.Phone)
+ else
+ KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .shadow(8.dp, RoundedCornerShape(8.dp))
+ .background(
+ LocalCustomColors.current.grayLight,
+ shape = RoundedCornerShape(8.dp)
+ )
+ .animateContentSize()
+ ) {
+ if (showSuggestions.value && filteredSuggestions.isNotEmpty()) {
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(max = 150.dp)) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScrollbar(
+ state = lazyListState,
+ color = LocalCustomColors.current.gray
+ ),
+ horizontalAlignment = Alignment.Start,
+ state = lazyListState,
+ ) {
+ items(
+ items = filteredSuggestions,
+ key = { suggestion -> suggestion }
+ ) { suggestion ->
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ transferUri = suggestion
+ showSuggestions.value = false
+ }
+ .padding(12.dp)
+ ) {
+ Text(
+ text = suggestion,
+ modifier = Modifier.fillMaxWidth(),
+ color = LocalCustomColors.current.grayDark,
+ fontSize = 18.sp
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ if (call != null && call.replaces())
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Start,
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = stringResource(R.string.blind),
+ color = LocalCustomColors.current.alert,
+ modifier = Modifier.padding(8.dp),
+ )
+ Switch(
+ checked = blindChecked.value,
+ onCheckedChange = {
+ blindChecked.value = true
+ }
+ )
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = stringResource(R.string.attended),
+ color = LocalCustomColors.current.alert,
+ modifier = Modifier.padding(8.dp),
+ )
+ Switch(
+ checked = !blindChecked.value,
+ onCheckedChange = {
+ blindChecked.value = false
+ }
+ )
+ }
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.End,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ TextButton(
+ onClick = {
+ keyboardController?.hide()
+ showDialog.value = false
+ showTransferDialog = false
+ },
+ modifier = Modifier.padding(end = 32.dp),
+ ) {
+ Text(
+ text = stringResource(R.string.cancel),
+ color = LocalCustomColors.current.gray
+ )
+ }
+ TextButton(
+ onClick = {
+ showSuggestions.value = false
+ var uriText = transferUri.trim()
+ if (uriText.isNotEmpty()) {
+ val uris = Contact.contactUris(uriText)
+ if (uris.size > 1) {
+ selectItems.value = uris
+ selectItemAction.value = { index ->
+ val uri = uris[index]
+ transfer(
+ ctx,
+ viewModel,
+ ua,
+ if (Utils.isTelNumber(uri)) "tel:$uri" else uri,
+ !blindChecked.value
+ )
+ showSelectItemDialog.value = false
+ }
+ showSelectItemDialog.value = true
+ }
+ else {
+ if (uris.size == 1) uriText = uris[0]
+ transfer(
+ ctx,
+ viewModel,
+ ua,
+ if (Utils.isTelNumber(uriText)) "tel:$uriText" else uriText,
+ !blindChecked.value
+ )
+ }
+ keyboardController?.hide()
+ showDialog.value = false
+ showTransferDialog = false
+ }
+ },
+ modifier = Modifier.padding(end = 16.dp),
+ ) {
+ Text(
+ text = stringResource(
+ if (blindChecked.value)
+ R.string.transfer
+ else
+ R.string.call
+ ).uppercase(),
+ color = LocalCustomColors.current.primary
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ val focusRequester = remember { FocusRequester() }
+ val shouldRequestFocus by focusDtmf
+ TextField(
+ value = dtmfText.value,
+ onValueChange = {
+ if (it.length > dtmfText.value.length) {
+ val char = it.last()
+ if (char.isDigit() || char == '*' || char == '#') {
+ Log.d(TAG, "Got DTMF digit '$char'")
+ val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
+ ua.currentCall()?.sendDigit(char)
+ }
+ }
+ dtmfText.value = it
+ },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
+ modifier = Modifier
+ .width(80.dp)
+ .focusRequester(focusRequester),
+ enabled = dtmfEnabled.value,
+ textStyle = TextStyle(fontSize = 16.sp),
+ label = { LabelText(stringResource(R.string.dtmf)) },
+ singleLine = true
+ )
+ LaunchedEffect(shouldRequestFocus) {
+ if (shouldRequestFocus) {
+ focusRequester.requestFocus()
+ focusDtmf.value = false
+ }
+ }
+
+ IconButton(
+ modifier = Modifier.size(48.dp),
+ onClick = {
+ val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
+ val call = ua.currentCall()
+ val stats = call?.stats("audio")
+ if (stats != null && call.startTime != null && stats != "") {
+ val parts = stats.split(",") as java.util.ArrayList
+ if (parts[2] == "0/0") {
+ parts[2] = "?/?"
+ parts[3] = "?/?"
+ parts[4] = "?/?"
+ }
+ val codecs = call.audioCodecs()
+ val duration = call.duration()
+ val txCodec = codecs.split(',')[0].split("/")
+ val rxCodec = codecs.split(',')[1].split("/")
+ alertTitle.value = ctx.getString(R.string.call_info)
+ alertMessage.value =
+ "${String.format(ctx.getString(R.string.duration), duration)}\n" +
+ "${ctx.getString(R.string.codecs)}: ${txCodec[0]} ch ${txCodec[2]}/" +
+ "${rxCodec[0]} ch ${rxCodec[2]}\n" +
+ "${String.format(ctx.getString(R.string.rate), parts[0])}\n" +
+ "${String.format(ctx.getString(R.string.average_rate), parts[1])}\n" +
+ "${ctx.getString(R.string.packets)}: ${parts[2]}\n" +
+ "${ctx.getString(R.string.lost)}: ${parts[3]}\n" +
+ String.format(ctx.getString(R.string.jitter), parts[4])
+ showAlert.value = true
+ } else {
+ alertTitle.value = ctx.getString(R.string.call_info)
+ alertMessage.value = ctx.getString(R.string.call_info_not_available)
+ showAlert.value = true
+ }
+ },
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.info),
+ modifier = Modifier.size(36.dp),
+ tint = Color.Unspecified,
+ contentDescription = null,
+ )
+ }
+ }
+
+ if (showAnswerRejectButtons.value) {
+
+ IconButton(
+ modifier = Modifier.size(48.dp),
+ onClick = {
+ answer(ctx, viewModel)
+ },
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.call),
+ modifier = Modifier.size(48.dp),
+ tint = Color.Unspecified,
+ contentDescription = null,
+ )
+ }
+
+ Spacer(Modifier.weight(1f))
+
+ IconButton(
+ modifier = Modifier.size(48.dp),
+ onClick = {
+ reject(viewModel)
+ },
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.hangup),
+ modifier = Modifier.size(48.dp),
+ tint = Color.Unspecified,
+ contentDescription = null,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun OnHoldNotice() {
+ OutlinedButton(
+ onClick = {},
+ border = BorderStroke(1.dp, LocalCustomColors.current.accent),
+ modifier = Modifier.padding(16.dp),
+ shape = RoundedCornerShape(20)
+ ) {
+ Text(
+ text = stringResource(R.string.call_is_on_hold),
+ fontSize = 18.sp,
+ color = LocalCustomColors.current.itemText,
+ )
+ }
+}
+
+private fun spinToAor(viewModel: ViewModel, aor: String) {
+ if (aor != viewModel.selectedAor.value)
+ viewModel.updateSelectedAor(aor)
+ updateIcons(viewModel, Account.ofAor(aor))
+}
+
+private fun callClick(ctx: Context, viewModel: ViewModel) {
+ if (viewModel.selectedAor.value != "") {
+ if (Utils.checkPermissions(ctx, arrayOf(RECORD_AUDIO))) {
+ if (Call.inCall())
+ return
+ val uriText = callUri.value.trim()
+ if (uriText.isNotEmpty()) {
+ val uris = Contact.contactUris(uriText)
+ if (uris.isEmpty())
+ makeCall(ctx, viewModel, uriText)
+ else if (uris.size == 1)
+ makeCall(ctx, viewModel, uris[0])
+ else {
+ selectItems.value = uris
+ selectItemAction.value = { index ->
+ makeCall(ctx, viewModel, uris[index])
+ }
+ showSelectItemDialog.value = true
+ }
+ }
+ else {
+ val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
+ val latestPeerUri = CallHistoryNew.aorLatestPeerUri(ua.account.aor)
+ if (latestPeerUri != null)
+ callUri.value = Utils.friendlyUri(ctx, latestPeerUri, ua.account)
+ }
+ }
+ else
+ Toast.makeText(ctx, R.string.no_calls, Toast.LENGTH_SHORT).show()
+ }
+}
+
+private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String) {
+ val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
+ val aor = ua.account.aor
+ val peerUri = if (Utils.isTelNumber(uriText))
+ "tel:$uriText"
+ else
+ uriText
+ val uri = if (Utils.isTelUri(peerUri)) {
+ if (ua.account.telProvider == "") {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.no_telephony_provider), aor)
+ showAlert.value = true
+ return
+ }
+ Utils.telToSip(peerUri, ua.account)
+ }
+ else
+ Utils.uriComplete(peerUri, aor)
+ if (!Utils.checkUri(uri)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri)
+ showAlert.value = true
+ }
+ else if (!BaresipService.requestAudioFocus(ctx))
+ Toast.makeText(ctx, R.string.audio_focus_denied, Toast.LENGTH_SHORT).show()
+ else {
+ if (Build.VERSION.SDK_INT < 31) {
+ Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
+ am.mode = AudioManager.MODE_IN_COMMUNICATION
+ runCall(ctx, viewModel, ua, uri)
+ } else {
+ if (am.mode == AudioManager.MODE_IN_COMMUNICATION) {
+ runCall(ctx, viewModel, ua, uri)
+ } else {
+ audioModeChangedListener = AudioManager.OnModeChangedListener { mode ->
+ if (mode == AudioManager.MODE_IN_COMMUNICATION) {
+ Log.d(TAG, "Audio mode changed to MODE_IN_COMMUNICATION using " +
+ "device ${am.communicationDevice!!.type}")
+ if (audioModeChangedListener != null) {
+ am.removeOnModeChangedListener(audioModeChangedListener!!)
+ audioModeChangedListener = null
+ }
+ runCall(ctx, viewModel, ua, uri)
+ } else {
+ Log.d(TAG, "Audio mode changed to mode ${am.mode} using " +
+ "device ${am.communicationDevice!!.type}")
+ }
+ }
+ am.addOnModeChangedListener(ctx.mainExecutor, audioModeChangedListener!!)
+ Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
+ am.mode = AudioManager.MODE_IN_COMMUNICATION
+ }
+ }
+ }
+}
+
+private fun runCall(ctx: Context, viewModel: ViewModel, ua: UserAgent, uri: String) {
+ callRunnable = Runnable {
+ callRunnable = null
+ if (!call(ctx, viewModel, ua, uri)) {
+ BaresipService.abandonAudioFocus(ctx)
+ showCallButton.value = true
+ showCancelButton.value = false
+ }
+ else {
+ showCallButton.value = false
+ showCancelButton.value = true
+ }
+ }
+ callHandler.postDelayed(callRunnable!!, BaresipService.audioDelay)
+}
+
+private fun call(
+ ctx: Context,
+ viewModel: ViewModel,
+ ua: UserAgent,
+ uri: String,
+ onHoldCall: Call? = null
+): Boolean {
+ spinToAor(viewModel, ua.account.aor)
+ val callp = ua.callAlloc(0L, Api.VIDMODE_OFF)
+ return if (callp != 0L) {
+ Log.d(TAG, "Adding outgoing call ${ua.uap}/$callp/$uri")
+ val call = Call(callp, ua, uri, "out", "outgoing")
+ call.onHoldCall = onHoldCall
+ call.add()
+ if (onHoldCall != null)
+ onHoldCall.newCall = call
+ if (call.connect(uri)) {
+ showCall(ctx, viewModel, ua)
+ true
+ } else {
+ Log.w(TAG, "call_connect $callp failed")
+ if (onHoldCall != null)
+ onHoldCall.newCall = null
+ call.remove()
+ call.destroy()
+ showCall(ctx, viewModel, ua)
+ false
+ }
+ } else {
+ Log.w(TAG, "callAlloc for ${ua.uap} to $uri failed")
+ false
+ }
+}
+
+private fun answer(ctx: Context, viewModel: ViewModel) {
+ val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
+ val call = ua.currentCall()
+ if (call != null) {
+ Log.d(TAG, "AoR ${ua.account.aor} answering call from ${callUri.value}")
+ val intent = Intent(ctx, BaresipService::class.java)
+ intent.action = "Call Answer"
+ intent.putExtra("uap", ua.uap)
+ intent.putExtra("callp", call.callp)
+ intent.putExtra("video", Api.VIDMODE_OFF)
+ ctx.startService(intent)
+ }
+}
+
+private fun reject(viewModel: ViewModel) {
+ val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
+ val call = ua.currentCall()
+ if (call != null) {
+ val callp = call.callp
+ Log.d(TAG, "AoR ${ua.account.aor} rejecting call $callp from ${callUri.value}")
+ call.rejected = true
+ Api.ua_hangup(ua.uap, callp, 486, "Busy Here")
+ }
+}
+
+private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText: String, attended: Boolean) {
+ val uri = if (Utils.isTelUri(uriText))
+ Utils.telToSip(uriText, ua.account)
+ else
+ Utils.uriComplete(uriText, ua.account.aor)
+ if (!Utils.checkUri(uri)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri)
+ showAlert.value = true
+ }
+ else {
+ val call = ua.currentCall()
+ if (call != null) {
+ if (attended) {
+ if (call.hold()) {
+ call.referTo = uri
+ call(ctx, viewModel, ua, uri, call)
+ }
+ }
+ else {
+ if (!call.transfer(uri)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = ctx.getString(R.string.transfer_failed)
+ showAlert.value = true
+ }
+ }
+ showCall(ctx, viewModel, ua)
+ }
+ }
+}
+
+private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCall: Call? = null) {
+ if (ua == null)
+ return
+ val call = showCall ?: ua.currentCall()
+ if (call == null) {
+ pullToRefreshEnabled = true
+ if (ua.account.resumeUri != "")
+ callUri.value = ua.account.resumeUri
+ else
+ callUri.value = ""
+ callUriLabel.value = ctx.getString(R.string.outgoing_call_to_dots)
+ callUriEnabled.value = true
+ keyboardController?.hide()
+ showCallTimer.value = false
+ securityIcon.intValue = -1
+ showHangupButton.value = false
+ transferIcon.intValue = R.drawable.call_transfer
+ dtmfEnabled.value = false
+ focusDtmf.value = false
+ showCallButton.value = true
+ showCancelButton.value = false
+ showAnswerRejectButtons.value = false
+ showOnHoldNotice.value = false
+ dialpadButtonEnabled = true
+ if (BaresipService.isMicMuted) {
+ BaresipService.isMicMuted = false
+ viewModel.updateMicIcon(R.drawable.mic_on)
+ }
+ } else {
+ pullToRefreshEnabled = false
+ callUriEnabled.value = false
+ when (call.status) {
+ "outgoing", "transferring", "answered" -> {
+ callUriLabel.value = if (call.status == "answered")
+ ctx.getString(R.string.incoming_call_from_dots)
+ else
+ ctx.getString(R.string.outgoing_call_to_dots)
+ callUri.value = Utils.friendlyUri(ctx, call.peerUri, ua.account)
+ showCallTimer.value = false
+ securityIcon.intValue = -1
+ showCallButton.value = false
+ showCancelButton.value = call.status == "outgoing"
+ showHangupButton.value = !showCancelButton.value
+ showAnswerRejectButtons.value = false
+ showOnHoldNotice.value = false
+ dialpadButtonEnabled = false
+ }
+ "incoming" -> {
+ showCallTimer.value = false
+ securityIcon.intValue = -1
+ val uri = call.diverterUri()
+ if (uri != "") {
+ callUriLabel.value = ctx.getString(R.string.diverted_by_dots)
+ callUri.value = Utils.friendlyUri(ctx, uri, ua.account)
+ }
+ else {
+ callUriLabel.value = ctx.getString(R.string.incoming_call_from_dots)
+ callUri.value = Utils.friendlyUri(ctx, call.peerUri, ua.account)
+ }
+ showCallButton.value = false
+ showCancelButton.value = false
+ showHangupButton.value = false
+ showAnswerRejectButtons.value = true
+ showOnHoldNotice.value = false
+ dialpadButtonEnabled = false
+ }
+ "connected" -> {
+ if (call.referTo != "") {
+ callUriLabel.value = ctx.getString(R.string.outgoing_call_to_dots)
+ callUri.value = Utils.friendlyUri(ctx, call.referTo, ua.account)
+ transferButtonEnabled.value = false
+ } else {
+ if (call.dir == "out") {
+ callUriLabel.value = ctx.getString(R.string.outgoing_call_to_dots)
+ callUri.value = Utils.friendlyUri(ctx, call.peerUri, ua.account)
+ } else {
+ callUriLabel.value = ctx.getString(R.string.incoming_call_from_dots)
+ callUri.value = Utils.friendlyUri(ctx, call.peerUri, ua.account)
+ }
+ transferButtonEnabled.value = true
+ }
+ transferIcon.intValue = if (call.onHoldCall == null)
+ R.drawable.call_transfer
+ else
+ R.drawable.call_transfer_execute
+ callDuration = call.duration()
+ showCallTimer.value = true
+ if (ua.account.mediaEnc == "")
+ securityIcon.intValue = -1
+ else
+ securityIcon.intValue = call.security
+ showCallButton.value = false
+ showCancelButton.value = false
+ showHangupButton.value = true
+ showAnswerRejectButtons.value = false
+ if (call.onhold)
+ holdIcon.intValue = R.drawable.resume
+ else
+ holdIcon.intValue = R.drawable.call_hold
+ Handler(Looper.getMainLooper()).postDelayed({
+ showOnHoldNotice.value = call.held
+ }, 100)
+ if (call.held) {
+ keyboardController?.hide()
+ dtmfEnabled.value = false
+ focusDtmf.value = false
+ } else {
+ dtmfEnabled.value = true
+ focusDtmf.value = true
+ if (ctx.resources.configuration.orientation == ORIENTATION_PORTRAIT)
+ keyboardController?.show()
+ }
+ }
+ }
+ }
+}
+
+fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params: ArrayList) {
+
+ fun handleNextEvent(logMessage: String? = null) {
+ if (logMessage != null)
+ Log.w(TAG, logMessage)
+ if (BaresipService.serviceEvents.isNotEmpty()) {
+ val first = BaresipService.serviceEvents.removeAt(0)
+ handleServiceEvent(ctx, viewModel, first.event, first.params)
+ }
+ }
+
+ if (event == "started") {
+ val uriString = params[0] as String
+ Log.d(TAG, "Handling service event 'started' with URI '$uriString'")
+ if (uriString != "")
+ callAction(ctx, viewModel, uriString.toUri(), "dial")
+ else {
+ if (viewModel.selectedAor.value == "" && uas.value.isNotEmpty())
+ viewModel.updateSelectedAor(uas.value.first().account.aor)
+ }
+ if (Preferences(ctx).displayTheme != AppCompatDelegate.getDefaultNightMode()) {
+ AppCompatDelegate.setDefaultNightMode(Preferences(ctx).displayTheme)
+ }
+ handleNextEvent()
+ return
+ }
+
+ val uap = params[0] as Long
+ val ua = UserAgent.ofUap(uap)
+ if (ua == null) {
+ handleNextEvent("handleServiceEvent '$event' did not find ua $uap")
+ return
+ }
+
+ val ev = event.split(",")
+ Log.d(TAG, "Handling service event '${ev[0]}' for $uap")
+ val acc = ua.account
+ val aor = ua.account.aor
+
+ when (ev[0]) {
+ "call rejected" -> {
+ if (aor == viewModel.selectedAor.value)
+ viewModel.updateCallsIcon(R.drawable.calls_missed)
+ }
+ "call incoming", "call outgoing" -> {
+ val callp = params[1] as Long
+ if (!BaresipService.isMainVisible)
+ viewModel.navigateToHome()
+ spinToAor(viewModel, aor)
+ showCall(ctx, viewModel, ua, Call.ofCallp(callp))
+ }
+ "call answered" -> {
+ if (!BaresipService.isMainVisible)
+ viewModel.navigateToHome()
+ spinToAor(viewModel, aor)
+ showCall(ctx, viewModel, ua)
+ }
+ "call redirect" -> {
+ val redirectUri = ev[1]
+ val target = Utils.friendlyUri(ctx, redirectUri, acc)
+ if (acc.autoRedirect) {
+ redirect(ctx, viewModel, ua, target)
+ Toast.makeText(ctx,
+ String.format(ctx.getString(R.string.redirect_notice), target),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ else {
+ dialogTitle.value = ctx.getString(R.string.redirect_request)
+ dialogMessage.value = String.format(ctx.getString(R.string.redirect_request_query), target)
+ positiveText.value = ctx.getString(R.string.yes)
+ onPositiveClicked.value = {
+ redirect(ctx, viewModel, ua, target)
+ }
+ negativeText.value = ctx.getString(R.string.no)
+ onNegativeClicked.value = {}
+ showDialog.value = true
+ }
+ showCall(ctx, viewModel, ua)
+ }
+ "call established" -> {
+ if (aor == viewModel.selectedAor.value) {
+ dtmfText.value = ""
+ showCall(ctx, viewModel, ua)
+ }
+ }
+ "call update" -> {
+ showCall(ctx, viewModel, ua)
+ }
+ "call verify" -> {
+ val callp = params[1] as Long
+ val call = Call.ofCallp(callp)
+ if (call == null) {
+ handleNextEvent("Call $callp to be verified is not found")
+ return
+ }
+ dialogTitle.value = ctx.getString(R.string.verify)
+ dialogMessage.value = String.format(ctx.getString(R.string.verify_sas), ev[1])
+ positiveText.value = ctx.getString(R.string.yes)
+ onPositiveClicked.value = {
+ call.security = if (Api.cmd_exec("zrtp_verify ${ev[2]}") != 0) {
+ Log.e(TAG, "Command 'zrtp_verify ${ev[2]}' failed")
+ R.drawable.locked_yellow
+ } else {
+ R.drawable.locked_green
+ }
+ call.zid = ev[2]
+ if (aor == viewModel.selectedAor.value)
+ securityIcon.intValue = call.security
+ }
+ negativeText.value = ctx.getString(R.string.no)
+ onNegativeClicked.value = {
+ call.security = R.drawable.locked_yellow
+ call.zid = ev[2]
+ if (aor == viewModel.selectedAor.value)
+ securityIcon.intValue = R.drawable.locked_yellow
+ onNegativeClicked.value = {}
+ }
+ showDialog.value = true
+ }
+ "call verified", "call secure" -> {
+ val callp = params[1] as Long
+ val call = Call.ofCallp(callp)
+ if (call == null) {
+ handleNextEvent("Call $callp that is verified is not found")
+ return
+ }
+ if (aor == viewModel.selectedAor.value)
+ securityIcon.intValue = call.security
+ }
+ "call transfer", "transfer show" -> {
+ if (!BaresipService.isMainVisible)
+ viewModel.navigateToHome()
+ val callp = params[1] as Long
+ val call = Call.ofCallp(callp)
+ val target = Utils.friendlyUri(ctx, ev[1], acc)
+ dialogTitle.value = if (call != null)
+ ctx.getString(R.string.transfer_request)
+ else
+ ctx.getString(R.string.call_request)
+ dialogMessage.value = if (call != null)
+ String.format(ctx.getString(R.string.transfer_request_query), target)
+ else
+ String.format(ctx.getString(R.string.call_request_query), target)
+ positiveText.value = ctx.getString(R.string.yes)
+ onPositiveClicked.value = {
+ if (call in Call.calls())
+ acceptTransfer(ctx, viewModel, ua, call!!, ev[1])
+ else
+ makeCall(ctx, viewModel, ev[1])
+ }
+ negativeText.value = ctx.getString(R.string.no)
+ onNegativeClicked.value = {
+ if (call in Call.calls())
+ call!!.notifySipfrag(603, "Decline")
+ onNegativeClicked.value = {}
+ }
+ showDialog.value = true
+ }
+ "transfer accept" -> {
+ val callp = params[1] as Long
+ val call = Call.ofCallp(callp)
+ if (call in Call.calls())
+ Api.ua_hangup(uap, callp, 0, "")
+ call(ctx, viewModel, ua, ev[1])
+ showCall(ctx, viewModel, ua)
+ }
+ "transfer failed" -> {
+ showCall(ctx, viewModel, ua)
+ }
+ "call closed" -> {
+ val call = ua.currentCall()
+ if (call != null) {
+ call.resume()
+ callDuration = call.duration()
+ showCallTimer.value = true
+ }
+ else
+ showCallTimer.value = false
+ if (aor == viewModel.selectedAor.value) {
+ ua.account.resumeUri = ""
+ showCall(ctx, viewModel, ua)
+ if (acc.missedCalls)
+ viewModel.updateCallsIcon(R.drawable.calls_missed)
+ }
+ //if (kgm.isDeviceLocked)
+ // this.setShowWhenLocked(false)
+ }
+ "message", "message show", "message reply" -> {
+ Handler(Looper.getMainLooper()).postDelayed({
+ viewModel.onNewMessageReceived(aor, params[1] as String)
+ }, 200)
+ }
+ "mwi notify" -> {
+ val lines = ev[1].split("\n")
+ for (line in lines) {
+ if (line.startsWith("Voice-Message:")) {
+ val counts = (line.split(" ")[1]).split("/")
+ acc.vmNew = counts[0].toInt()
+ acc.vmOld = counts[1].toInt()
+ break
+ }
+ }
+ if (aor == viewModel.selectedAor.value) {
+ viewModel.updateVmIcon(if (acc.vmNew > 0)
+ R.drawable.voicemail_new
+ else
+ R.drawable.voicemail
+ )
+ }
+ }
+ else -> Log.e(TAG, "Unknown event '${ev[0]}'")
+ }
+
+ handleNextEvent()
+}
+
+fun handleIntent(ctx: Context, viewModel: ViewModel, intent: Intent, action: String) {
+ Log.d(TAG, "Handling intent '$action'")
+ val ev = action.split(",")
+ when (ev[0]) {
+ "call", "dial" -> {
+ if (Call.inCall()) {
+ Toast.makeText(ctx, ctx.getString(R.string.call_already_active),
+ Toast.LENGTH_SHORT).show()
+ return
+ }
+ val uap = intent.getLongExtra("uap", 0L)
+ val ua = UserAgent.ofUap(uap)
+ if (ua == null) {
+ Log.w(TAG, "handleIntent 'call' did not find ua $uap")
+ return
+ }
+ callUri.value = intent.getStringExtra("peer")!!
+ spinToAor(viewModel, ua.account.aor)
+ if (ev[0] == "call")
+ callClick(ctx, viewModel)
+ }
+ "call show", "call answer" -> {
+ val callp = intent.getLongExtra("callp", 0L)
+ val call = Call.ofCallp(callp)
+ if (call == null) {
+ Log.w(TAG, "handleIntent '$action' did not find call $callp")
+ return
+ }
+ val ua = call.ua
+ spinToAor(viewModel, ua.account.aor)
+ if (ev[0] == "call answer") {
+ answer(ctx, viewModel)
+ showCall(ctx, viewModel, ua, call)
+ }
+ else
+ BaresipService.postServiceEvent(ServiceEvent(
+ "call incoming",
+ arrayListOf(call.ua.uap, callp),
+ System.nanoTime())
+ )
+ }
+ "call missed" -> {
+ val uap = intent.getLongExtra("uap", 0L)
+ val ua = UserAgent.ofUap(uap)
+ if (ua == null) {
+ Log.w(TAG, "handleIntent did not find ua $uap")
+ return
+ }
+ spinToAor(viewModel, ua.account.aor)
+ viewModel.navigateToCalls(ua.account.aor)
+ }
+ "call transfer", "transfer show", "transfer accept" -> {
+ val callp = intent.getLongExtra("callp", 0L)
+ val call = Call.ofCallp(callp)
+ if (call == null) {
+ Log.w(TAG, "handleIntent '$action' did not find call $callp")
+ // moveTaskToBack(true)
+ return
+ }
+ val uri = if (ev[0] == "call transfer")
+ ev[1]
+ else
+ intent.getStringExtra("uri")!!
+ BaresipService.postServiceEvent(ServiceEvent(
+ ev[0] + "," + uri,
+ arrayListOf(call.ua.uap, callp),
+ System.nanoTime())
+ )
+ }
+ "message", "message show", "message reply" -> {
+ val uap = intent.getLongExtra("uap", 0L)
+ val ua = UserAgent.ofUap(uap)
+ if (ua == null) {
+ Log.w(TAG, "handleIntent did not find ua $uap")
+ return
+ }
+ spinToAor(viewModel, ua.account.aor)
+ BaresipService.postServiceEvent(ServiceEvent(
+ ev[0],
+ arrayListOf(uap, intent.getStringExtra("peer")!!),
+ System.nanoTime())
+ )
+ }
+ }
+}
+
+fun handleDialog(ctx: Context, title: String, message: String, action: () -> Unit = {}) {
+ dialogTitle.value = title
+ dialogMessage.value = message
+ positiveText.value = ctx.getString(R.string.ok)
+ onPositiveClicked.value = { action() }
+ negativeText.value = ""
+ showDialog.value = true
+}
+
+fun callAction(ctx: Context, viewModel: ViewModel, uri: Uri?, action: String) {
+ if (Call.inCall() || uas.value.isEmpty())
+ return
+ Log.d(TAG, "Action $action to $uri")
+ if (uri != null) {
+ var uriStr: String
+ var uap = 0L
+ when (uri.scheme) {
+ "sip" -> {
+ uriStr = Utils.uriUnescape(uri.toString())
+ var ua = UserAgent.ofDomain(Utils.uriHostPart(uriStr))
+ if (ua == null && uas.value.isNotEmpty())
+ ua = uas.value[0]
+ if (ua == null) {
+ Log.w(TAG, "No accounts for '$uriStr'")
+ return
+ }
+ uap = ua.uap
+ }
+ "tel" -> {
+ uriStr = uri.toString().replace("%2B", "+").replace("%20", "")
+ .filterNot { setOf('-', ' ', '(', ')').contains(it) }
+ var account: Account? = null
+ for (a in Account.accounts())
+ if (a.telProvider != "") {
+ account = a
+ break
+ }
+ if (account == null) {
+ Log.w(TAG, "No telephony providers for '$uriStr'")
+ return
+ }
+ uap = UserAgent.ofAor(account.aor)!!.uap
+ }
+ else -> {
+ Log.w(TAG, "Unsupported URI scheme ${uri.scheme}")
+ return
+ }
+ }
+ val intent = Intent(ctx, MainActivity::class.java)
+ intent.putExtra("uap", uap)
+ intent.putExtra("peer", uriStr)
+ handleIntent(ctx, viewModel, intent, action)
+ }
+}
+
+private fun redirect(ctx: Context, viewModel: ViewModel, ua: UserAgent, redirectUri: String) {
+ if (ua.account.aor != viewModel.selectedAor.value)
+ spinToAor(viewModel, ua.account.aor)
+ callUri.value = redirectUri
+ callClick(ctx, viewModel)
+}
+
+private fun acceptTransfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, call: Call, uri: String) {
+ val newCallp = ua.callAlloc(call.callp, Api.VIDMODE_OFF)
+ if (newCallp != 0L) {
+ Log.d(TAG, "Adding outgoing call ${ua.uap}/$newCallp/$uri")
+ val newCall = Call(newCallp, ua, uri, "out", "transferring")
+ newCall.add()
+ if (newCall.connect(uri)) {
+ if (ua.account.aor != viewModel.selectedAor.value)
+ spinToAor(viewModel, ua.account.aor)
+ showCall(ctx, viewModel, ua)
+ } else {
+ Log.w(TAG, "call_connect $newCallp failed")
+ call.notifySipfrag(500, "Call Error")
+ }
+ } else {
+ Log.w(TAG, "callAlloc for ua ${ua.uap} call ${call.callp} transfer failed")
+ call.notifySipfrag(500, "Call Error")
+ }
+}
+
+private fun updateIcons(viewModel: ViewModel, acc: Account?) {
+ if (acc == null) {
+ viewModel.updateShowVmIcon(false)
+ viewModel.updateMessagesIcon(R.drawable.messages)
+ viewModel.updateCallsIcon(R.drawable.calls)
+ }
+ else {
+
+ if (acc.vmUri != "") {
+ viewModel.updateShowVmIcon(true)
+ viewModel.updateVmIcon(if (acc.vmNew > 0)
+ R.drawable.voicemail_new
+ else
+ R.drawable.voicemail
+ )
+ } else
+ viewModel.updateShowVmIcon(false)
+
+ viewModel.updateMessagesIcon(if (acc.unreadMessages)
+ R.drawable.messages_unread
+ else
+ R.drawable.messages)
+
+ viewModel.updateCallsIcon(if (acc.missedCalls)
+ R.drawable.calls_missed
+ else
+ R.drawable.calls)
+ }
+}
+
+private fun backup(ctx: Context, password: String) {
+ val files = arrayListOf("accounts", "config", "contacts", "call_history",
+ "messages", "gzrtp.zid", "cert.pem", "ca_cert", "ca_certs.crt")
+ File(BaresipService.filesPath).walk().forEach {
+ if (it.name.endsWith(".png"))
+ files.add(it.name)
+ }
+ val zipFile = ctx.getString(R.string.app_name) + ".zip"
+ val zipFilePath = BaresipService.filesPath + "/$zipFile"
+ if (!Utils.zip(files, zipFile)) {
+ Log.w(TAG, "Failed to write zip file '$zipFile'")
+ alertTitle.value = ctx.getString(R.string.error)
+ alertMessage.value = String.format(ctx.getString(R.string.backup_failed),
+ Utils.fileNameOfUri(ctx, downloadsOutputUri!!))
+ showAlert.value = true
+ downloadsOutputUri = null
+ return
+ }
+ val content = Utils.getFileContents(zipFilePath)
+ if (content == null) {
+ Log.w(TAG, "Failed to read zip file '$zipFile'")
+ alertTitle.value = ctx.getString(R.string.error)
+ alertMessage.value = String.format(ctx.getString(R.string.backup_failed),
+ Utils.fileNameOfUri(ctx, downloadsOutputUri!!))
+ showAlert.value = true
+ downloadsOutputUri = null
+ return
+ }
+ if (!Utils.encryptToUri(ctx, downloadsOutputUri!!, content, password)) {
+ alertTitle.value = ctx.getString(R.string.error)
+ alertMessage.value = String.format(ctx.getString(R.string.backup_failed),
+ Utils.fileNameOfUri(ctx, downloadsOutputUri!!))
+ showAlert.value = true
+ downloadsOutputUri = null
+ return
+ }
+ alertTitle.value = ctx.getString(R.string.info)
+ alertMessage.value = String.format(ctx.getString(R.string.backed_up),
+ Utils.fileNameOfUri(ctx, downloadsOutputUri!!))
+ showAlert.value = true
+ Utils.deleteFile(File(zipFilePath))
+ downloadsOutputUri = null
+}
+
+private fun restore(ctx: Context, password: String, onRestartApp: () -> Unit) {
+ val zipFile = ctx.getString(R.string.app_name) + ".zip"
+ val zipFilePath = BaresipService.filesPath + "/$zipFile"
+ val zipData = Utils.decryptFromUri(ctx, downloadsInputUri!!, password)
+ if (zipData == null) {
+ alertTitle.value = ctx.getString(R.string.error)
+ alertMessage.value = String.format(ctx.getString(R.string.restore_failed),
+ Utils.fileNameOfUri(ctx, downloadsOutputUri!!))
+ showAlert.value = true
+ downloadsOutputUri = null
+ return
+ }
+ if (!Utils.putFileContents(zipFilePath, zipData)) {
+ Log.w(TAG, "Failed to write zip file '$zipFile'")
+ alertTitle.value = ctx.getString(R.string.error)
+ alertMessage.value = String.format(ctx.getString(R.string.restore_failed),
+ Utils.fileNameOfUri(ctx, downloadsOutputUri!!))
+ showAlert.value = true
+ downloadsOutputUri = null
+ return
+ }
+ if (!Utils.unZip(zipFilePath)) {
+ Log.w(TAG, "Failed to unzip file '$zipFile'")
+ alertTitle.value = ctx.getString(R.string.error)
+ alertMessage.value = String.format(
+ ctx.getString(R.string.restore_unzip_failed),
+ "baresip",
+ BuildConfig.VERSION_NAME
+ )
+ showAlert.value = true
+ downloadsOutputUri = null
+ return
+ }
+ Utils.deleteFile(File(zipFilePath))
+
+ File("${BaresipService.filesPath}/recordings").walk().forEach {
+ if (it.name.startsWith("dump"))
+ Utils.deleteFile(it)
+ }
+
+ dialogTitle.value = ctx.getString(R.string.info)
+ dialogMessage.value = ctx.getString(R.string.restored)
+ positiveText.value = ctx.getString(R.string.restart)
+ onPositiveClicked.value = {
+ onRestartApp()
+ showDialog.value = false
+ }
+ negativeText.value = ctx.getString(R.string.cancel)
+ onNegativeClicked.value = {
+ showDialog.value = false
+ }
+ showDialog.value = true
+
+ downloadsOutputUri = null
+}
+
+private fun abandonAudioFocus(ctx: Context) {
+ if (Build.VERSION.SDK_INT < 31) {
+ if (callRunnable != null) {
+ callHandler.removeCallbacks(callRunnable!!)
+ callRunnable = null
+ BaresipService.abandonAudioFocus(ctx)
+ }
+ } else {
+ if (audioModeChangedListener != null) {
+ val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ am.removeOnModeChangedListener(audioModeChangedListener!!)
+ audioModeChangedListener = null
+ BaresipService.abandonAudioFocus(ctx)
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/Message.kt b/app/src/main/kotlin/com/tutpro/baresip/Message.kt
index f0670601..0c381062 100644
--- a/app/src/main/kotlin/com/tutpro/baresip/Message.kt
+++ b/app/src/main/kotlin/com/tutpro/baresip/Message.kt
@@ -52,7 +52,7 @@ class Message(val aor: String, val peerUri: String, val message: String, val tim
fun deleteAorMessage(aor: String, time: Long) {
val updatedMessages = BaresipService.messages.toMutableList()
- for (message in updatedMessages)
+ for (message in updatedMessages.reversed())
if (message.aor == aor && message.timeStamp == time) {
updatedMessages.remove(message)
BaresipService.messages = updatedMessages.toList()
@@ -72,7 +72,7 @@ class Message(val aor: String, val peerUri: String, val message: String, val tim
fun updateAorMessage(aor: String, time: Long) {
val updatedMessages = BaresipService.messages.toMutableList()
- for (message in updatedMessages)
+ for (message in updatedMessages.reversed())
if (message.aor == aor && message.timeStamp == time) {
message.new = false
BaresipService.messages = updatedMessages.toList()
@@ -81,6 +81,35 @@ class Message(val aor: String, val peerUri: String, val message: String, val tim
}
}
+ fun unreadMessages(aor: String): Boolean {
+ for (message in BaresipService.messages.reversed())
+ if (message.aor == aor && message.new)
+ return true
+ return false
+ }
+
+ fun unreadMessagesFromPeer(aor: String, peerUri: String): Boolean {
+ for (message in BaresipService.messages.reversed())
+ if (message.aor == aor && message.peerUri == peerUri && message.new)
+ return true
+ return false
+ }
+
+ fun updateMessagesFromPearRead(aor: String, peerUri: String): Boolean {
+ val updatedMessages = BaresipService.messages.toMutableList()
+ var updated = false
+ for (message in updatedMessages)
+ if (message.aor == aor && message.peerUri == peerUri && message.new) {
+ message.new = false
+ updated = true
+ }
+ if (updated) {
+ BaresipService.messages = updatedMessages.toList()
+ save()
+ }
+ return updated
+ }
+
fun save() {
val file = File(BaresipService.filesPath, "messages")
try {
diff --git a/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt
new file mode 100644
index 00000000..f5541fb9
--- /dev/null
+++ b/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt
@@ -0,0 +1,1428 @@
+package com.tutpro.baresip
+
+import android.Manifest
+import android.app.Activity
+import android.app.Activity.RESULT_OK
+import android.app.role.RoleManager
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Context.POWER_SERVICE
+import android.content.Context.ROLE_SERVICE
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.Build.VERSION
+import android.os.PowerManager
+import android.provider.Settings
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.LocalActivity
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
+import androidx.core.content.ContextCompat
+import androidx.core.net.toUri
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import com.tutpro.baresip.CustomElements.AlertDialog
+import com.tutpro.baresip.CustomElements.LabelText
+import com.tutpro.baresip.CustomElements.verticalScrollbar
+import com.tutpro.baresip.Utils.copyInputStreamToFile
+import java.io.File
+import java.io.FileInputStream
+import java.util.Locale
+
+private var restart = false
+private val showRestartDialog = mutableStateOf(false)
+private var save = false
+
+fun NavGraphBuilder.settingsScreenRoute(
+ navController: NavController,
+ onRestartApp: () -> Unit
+) {
+ composable("settings") {
+ val ctx = LocalContext.current
+ SettingsScreen(
+ navController = navController,
+ onBack = { navController.popBackStack() },
+ checkOnClick = {
+ checkOnClick(ctx)
+ if (restart)
+ showRestartDialog.value = true
+ else
+ navController.popBackStack()
+ },
+ onRestartApp = onRestartApp
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun SettingsScreen(
+ navController: NavController,
+ onBack: () -> Unit,
+ checkOnClick: () -> Unit,
+ onRestartApp: () -> Unit
+) {
+ val activity = LocalActivity.current
+ val viewModel: ViewModel = viewModel(activity as ComponentActivity)
+ val audioResult by viewModel.audioSettingsResult
+ LaunchedEffect(audioResult) {
+ audioResult?.let { result ->
+ Log.d("SettingsScreen", "Got result from AudioSettings: $result")
+ restart = restart || result
+ viewModel.clearAudioSettingsResult()
+ }
+ }
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize().imePadding(),
+ containerColor = LocalCustomColors.current.background,
+ topBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LocalCustomColors.current.background)
+ .padding(
+ top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
+ )
+ ) {
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(R.string.configuration),
+ color = LocalCustomColors.current.light,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = null,
+ tint = LocalCustomColors.current.light
+ )
+ }
+ },
+ windowInsets = WindowInsets(0, 0, 0, 0),
+ actions = {
+ IconButton(onClick = checkOnClick) {
+ Icon(
+ imageVector = Icons.Filled.Check,
+ tint = LocalCustomColors.current.light,
+ contentDescription = "Check"
+ )
+ }
+ },
+ colors = TopAppBarDefaults.mediumTopAppBarColors(
+ containerColor = LocalCustomColors.current.primary
+ )
+ )
+ }
+ }
+ ) { contentPadding ->
+
+ if (showRestartDialog.value) {
+ AlertDialog(
+ showDialog = showRestartDialog,
+ title = stringResource(R.string.restart_request),
+ message = stringResource(R.string.config_restart),
+ positiveButtonText = stringResource(R.string.restart),
+ onPositiveClicked = {
+ onRestartApp()
+ },
+ negativeButtonText = stringResource(R.string.cancel),
+ onNegativeClicked = {
+ navController.popBackStack()
+ }
+ )
+ }
+
+ SettingsContent(contentPadding, navController, activity, onRestartApp)
+ }
+}
+
+private val dialogTitle = mutableStateOf("")
+private val dialogMessage = mutableStateOf("")
+private val positiveText = mutableStateOf("")
+private val onPositiveClicked = mutableStateOf({})
+private val negativeText = mutableStateOf("")
+private val onNegativeClicked = mutableStateOf({})
+private val showDialog = mutableStateOf(false)
+
+private val alertTitle = mutableStateOf("")
+private val alertMessage = mutableStateOf("")
+private val showAlert = mutableStateOf(false)
+
+private var oldAutoStart = false
+private var newAutoStart = false
+private var oldListenAddr = ""
+private var newListenAddr = ""
+private var oldAddressFamily = ""
+private var newAddressFamily = ""
+private var oldDnsServers = ""
+private var newDnsServers = ""
+private var oldTlsCertificateFile = false
+private var newTlsCertificateFile = false
+private var oldVerifyServer = false
+private var newVerifyServer = false
+private var oldCaFile = false
+private var newCaFile = false
+private var oldUserAgent = ""
+private var newUserAgent = ""
+private var oldRingtoneUri = ""
+private var newRingtoneUri = ""
+private var oldBatteryOptimizations = false
+private var newBatteryOptimizations = false
+private var oldDefaultDialer = false
+private var newDefaultDialer = false
+private var oldContactsMode = ""
+private var newContactsMode = ""
+private var oldDarkTheme = false
+private var newDarkTheme = false
+private var oldDebug = false
+private var newDebug = false
+private var oldSipTrace = false
+private var newSipTrace = false
+
+@Composable
+private fun SettingsContent(
+ contentPadding: PaddingValues,
+ navController: NavController,
+ activity: Activity,
+ onRestartApp: () -> Unit
+) {
+
+ if (showAlert.value) {
+ AlertDialog(
+ showDialog = showAlert,
+ title = alertTitle.value,
+ message = alertMessage.value,
+ positiveButtonText = stringResource(R.string.ok),
+ )
+ }
+
+ if (showDialog.value)
+ AlertDialog(
+ showDialog = showDialog,
+ title = dialogTitle.value,
+ message = dialogMessage.value,
+ positiveButtonText = positiveText.value,
+ onPositiveClicked = onPositiveClicked.value,
+ negativeButtonText = negativeText.value,
+ onNegativeClicked = onNegativeClicked.value,
+ )
+
+ val ctx = LocalContext.current
+
+ oldAutoStart = Config.variable("auto_start") == "yes"
+ if (oldAutoStart && !isAppearOnTopPermissionGranted(LocalContext.current)) {
+ Config.replaceVariable("auto_start", "no")
+ oldAutoStart = false
+ save = true
+ }
+ newAutoStart = oldAutoStart
+
+ oldListenAddr = Config.variable("sip_listen")
+
+ oldAddressFamily = Config.variable("net_af").lowercase()
+ newAddressFamily = oldAddressFamily
+
+ val dynamicDns = Config.variable("dyn_dns")
+ if (dynamicDns == "yes") {
+ oldDnsServers = ""
+ } else {
+ val servers = Config.variables("dns_server")
+ var serverList = ""
+ for (server in servers)
+ serverList += ", $server"
+ oldDnsServers = serverList.trimStart(',').trimStart(' ')
+ }
+
+ val certFile = File(BaresipService.filesPath + "/cert.pem")
+ oldTlsCertificateFile = certFile.exists()
+
+ oldVerifyServer = Config.variable("sip_verify_server") == "yes"
+ newVerifyServer = oldVerifyServer
+
+ val caCertsFile = File(BaresipService.filesPath + "/ca_certs.crt")
+ oldCaFile = caCertsFile.exists()
+
+ oldUserAgent = Config.variable("user_agent")
+ newUserAgent = oldUserAgent
+
+ val powerManager = ctx.getSystemService(POWER_SERVICE) as PowerManager
+ oldBatteryOptimizations = powerManager
+ .isIgnoringBatteryOptimizations(ctx.packageName) == false
+ newBatteryOptimizations = oldBatteryOptimizations
+
+ if (VERSION.SDK_INT >= 29) {
+ val roleManager = ctx.getSystemService(ROLE_SERVICE) as RoleManager
+ oldDefaultDialer = roleManager.isRoleHeld(RoleManager.ROLE_DIALER)
+ }
+
+ oldContactsMode = Config.variable("contacts_mode").lowercase()
+ newContactsMode = oldContactsMode
+
+ oldDarkTheme = Preferences(ctx).displayTheme == AppCompatDelegate.MODE_NIGHT_YES
+ newDarkTheme = oldDarkTheme
+
+ oldDebug = Config.variable("log_level") == "0"
+ newDebug = oldDebug
+
+ oldSipTrace = BaresipService.sipTrace
+ newSipTrace = oldSipTrace
+
+ val scrollState = rememberScrollState()
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(contentPadding)
+ .padding(start = 16.dp, end = 4.dp, top = 16.dp, bottom = 8.dp)
+ .verticalScrollbar(scrollState)
+ .verticalScroll(state = scrollState),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ StartAutomatically()
+ ListenAddress()
+ AddressFamily()
+ DnsServers()
+ TlsCertificateFile(activity)
+ VerifyServer()
+ CaFile(activity)
+ UserAgent()
+ AudioSettings(navController)
+ Contacts(activity)
+ Ringtone()
+ BatteryOptimizations()
+ DarkTheme()
+ if (VERSION.SDK_INT >= 29)
+ DefaultDialer()
+ Debug()
+ SipTrace()
+ Reset(onRestartApp)
+ }
+}
+
+@Composable
+private fun StartAutomatically() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.start_automatically),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.start_automatically)
+ alertMessage.value = ctx.getString(R.string.start_automatically_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ var startAutomatically by remember { mutableStateOf(oldAutoStart) }
+ Switch(
+ checked = startAutomatically,
+ onCheckedChange = {
+ if (it) {
+ if (!isAppearOnTopPermissionGranted(ctx)) {
+ dialogTitle.value = ctx.getString(R.string.notice)
+ dialogMessage.value = ctx.getString(R.string.appear_on_top_permission)
+ positiveText.value = ctx.getString(R.string.ok)
+ onPositiveClicked.value = {
+ val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
+ ctx.startActivity(intent)
+ }
+ negativeText.value = ctx.getString(R.string.cancel)
+ onNegativeClicked.value = {
+ negativeText.value = ""
+ }
+ showDialog.value = true
+ startAutomatically = false
+ }
+ else
+ startAutomatically = true
+ }
+ else
+ startAutomatically = false
+ newAutoStart = startAutomatically
+ }
+ )
+ }
+}
+
+@Composable
+private fun ListenAddress() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start,
+ ) {
+ val ctx = LocalContext.current
+ var listenAddr by remember { mutableStateOf(oldListenAddr) }
+ newListenAddr = listenAddr
+ OutlinedTextField(
+ value = listenAddr,
+ placeholder = { Text(stringResource(R.string._0_0_0_0_5060)) },
+ onValueChange = {
+ listenAddr = it
+ newListenAddr = listenAddr
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.listen_address)
+ alertMessage.value = ctx.getString(R.string.listen_address_help)
+ showAlert.value = true
+ },
+ textStyle = androidx.compose.ui.text.TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.listen_address)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun AddressFamily() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp)
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.address_family),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.address_family)
+ alertMessage.value = ctx.getString(R.string.address_family_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ val isDropDownExpanded = remember {
+ mutableStateOf(false)
+ }
+ val familyNames = listOf("--", "IPv4", "IPv6")
+ val familyValues = listOf("", "ipv4", "ipv6")
+ val itemPosition = remember {
+ mutableIntStateOf(familyValues.indexOf(oldAddressFamily))
+ }
+ Box {
+ Row(
+ horizontalArrangement = Arrangement.End,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.clickable {
+ isDropDownExpanded.value = true
+ }
+ ) {
+ Text(text = familyNames[itemPosition.intValue],
+ color = LocalCustomColors.current.itemText)
+ CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
+ tint = LocalCustomColors.current.itemText)
+ }
+ DropdownMenu(
+ expanded = isDropDownExpanded.value,
+ onDismissRequest = {
+ isDropDownExpanded.value = false
+ }) {
+ familyNames.forEachIndexed { index, family ->
+ DropdownMenuItem(text = {
+ Text(text = family)
+ },
+ onClick = {
+ isDropDownExpanded.value = false
+ itemPosition.intValue = index
+ newAddressFamily = familyValues[index]
+ })
+ if (index < 2)
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = LocalCustomColors.current.itemText
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun DnsServers() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ var dnsServers by remember { mutableStateOf(oldDnsServers) }
+ newDnsServers = dnsServers
+ OutlinedTextField(
+ value = dnsServers,
+ onValueChange = {
+ dnsServers = it
+ newDnsServers = dnsServers
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.dns_servers)
+ alertMessage.value = ctx.getString(R.string.dns_servers_help)
+ showAlert.value = true
+ },
+ textStyle = androidx.compose.ui.text.TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText
+ ),
+ label = { LabelText(stringResource(R.string.dns_servers)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun TlsCertificateFile(activity: Activity) {
+
+ val ctx = LocalContext.current
+
+ val requestPermissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission()
+ ) {}
+
+ val certificateRequest = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) {
+ val certPath = BaresipService.filesPath + "/cert.pem"
+ val certFile = File(certPath)
+ if (it.resultCode == RESULT_OK) {
+ it.data?.data?.also { uri ->
+ try {
+ val inputStream = ctx.contentResolver.openInputStream(uri) as FileInputStream
+ certFile.copyInputStreamToFile(inputStream)
+ inputStream.close()
+ Config.replaceVariable("sip_certificate", certPath)
+ save = true
+ restart = true
+ } catch (e: Error) {
+ alertTitle.value = ctx.getString(R.string.error)
+ alertMessage.value = ctx.getString(R.string.read_cert_error) + ": " + e.message
+ showAlert.value = true
+ newTlsCertificateFile = false
+ }
+ }
+ }
+ else
+ newTlsCertificateFile = false
+ if (!newTlsCertificateFile)
+ Utils.deleteFile(certFile)
+ }
+
+ val showAlertDialog = remember { mutableStateOf(false) }
+
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.tls_certificate_file),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.tls_certificate_file)
+ alertMessage.value = ctx.getString(R.string.tls_certificate_file_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ var tlsCertificateFile by remember { mutableStateOf(oldTlsCertificateFile) }
+ Switch(
+ checked = tlsCertificateFile,
+ onCheckedChange = {
+ tlsCertificateFile = it
+ newTlsCertificateFile = tlsCertificateFile
+ if (it)
+ if (VERSION.SDK_INT < 29) {
+ tlsCertificateFile = false
+ val permission = Manifest.permission.READ_EXTERNAL_STORAGE
+ when {
+ ContextCompat.checkSelfPermission(ctx, permission) ==
+ PackageManager.PERMISSION_GRANTED -> {
+ Log.d(TAG, "Read External Storage permission granted")
+ val downloadsPath = Utils.downloadsPath("cert.pem")
+ val content = Utils.getFileContents(downloadsPath)
+ if (content == null) {
+ alertTitle.value = ctx.getString(R.string.error)
+ alertMessage.value = ctx.getString(R.string.read_cert_error)
+ showAlert.value = true
+ return@Switch
+ }
+ val certPath = BaresipService.filesPath + "/cert.pem"
+ Utils.putFileContents(certPath, content)
+ Config.replaceVariable("sip_certificate", certPath)
+ tlsCertificateFile = true
+ save = true
+ restart = true
+ }
+ shouldShowRequestPermissionRationale(activity, permission) ->
+ showAlertDialog.value = true
+ else ->
+ requestPermissionLauncher.launch(permission)
+ }
+ }
+ else
+ Utils.selectInputFile(certificateRequest)
+ else {
+ Config.removeVariable("sip_certificate")
+ Utils.deleteFile(File(BaresipService.filesPath + "/cert.pem"))
+ save = true
+ restart = true
+ }
+ }
+ )
+ }
+
+ if (showAlertDialog.value)
+ AlertDialog(
+ showDialog = showAlertDialog,
+ title = stringResource(R.string.notice),
+ message = stringResource(R.string.no_read_permission),
+ positiveButtonText = stringResource(R.string.ok),
+ onPositiveClicked = { requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) },
+ negativeButtonText = "",
+ onNegativeClicked = {},
+ )
+}
+
+@Composable
+private fun VerifyServer() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.verify_server),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.verify_server)
+ alertMessage.value = ctx.getString(R.string.verify_server_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ var verifyServer by remember { mutableStateOf(oldVerifyServer) }
+ Switch(
+ checked = verifyServer,
+ onCheckedChange = {
+ verifyServer = it
+ newVerifyServer = verifyServer
+ }
+ )
+ }
+}
+
+@Composable
+private fun CaFile(activity: Activity) {
+
+ val ctx = LocalContext.current
+
+ val requestPermissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission()
+ ) {}
+
+ val caCertsRequest = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) {
+ val caCertsFile = File(BaresipService.filesPath + "/ca_certs.crt")
+ if (it.resultCode == RESULT_OK)
+ it.data?.data?.also { uri ->
+ try {
+ val inputStream = ctx.contentResolver.openInputStream(uri) as FileInputStream
+ caCertsFile.copyInputStreamToFile(inputStream)
+ inputStream.close()
+ restart = true
+ } catch (e: Error) {
+ alertTitle.value = ctx.getString(R.string.error)
+ alertMessage.value = ctx.getString(R.string.read_ca_certs_error) + ": " + e.message
+ showAlert.value = true
+ newCaFile = false
+ }
+ }
+ else
+ newCaFile = false
+ if (!newCaFile) caCertsFile.delete()
+ }
+
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.tls_ca_file),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.tls_ca_file)
+ alertMessage.value = ctx.getString(R.string.tls_ca_file_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ var caFile by remember { mutableStateOf(oldCaFile) }
+ Switch(
+ checked = caFile,
+ onCheckedChange = {
+ caFile = it
+ newCaFile = caFile
+ if (it) {
+ if (VERSION.SDK_INT < 29) {
+ caFile = false
+ val permission = Manifest.permission.READ_EXTERNAL_STORAGE
+ when {
+ ContextCompat.checkSelfPermission(ctx, permission) == PackageManager.PERMISSION_GRANTED -> {
+ Log.d(TAG, "Read External Storage permission granted")
+ val downloadsPath = Utils.downloadsPath("ca_certs.crt")
+ val content = Utils.getFileContents(downloadsPath)
+ if (content == null) {
+ alertTitle.value = ctx.getString(R.string.error)
+ alertMessage.value = ctx.getString(R.string.read_ca_certs_error)
+ showAlert.value = true
+ return@Switch
+ }
+ File(BaresipService.filesPath + "/ca_certs.crt").writeBytes(content)
+ caFile = true
+ restart = true
+ }
+ shouldShowRequestPermissionRationale(activity, permission) -> {
+ dialogTitle.value = ctx.getString(R.string.notice)
+ dialogMessage.value = ctx.getString(R.string.no_read_permission)
+ positiveText.value = ctx.getString(R.string.ok)
+ onPositiveClicked.value = {
+ requestPermissionLauncher.launch(permission)
+ }
+ negativeText.value = ""
+ showDialog.value = true
+ }
+ else ->
+ requestPermissionLauncher.launch(permission)
+ }
+ }
+ else
+ Utils.selectInputFile(caCertsRequest)
+ }
+ else {
+ Utils.deleteFile(File(BaresipService.filesPath + "/ca_certs.crt"))
+ restart = true
+ }
+ }
+ )
+ }
+}
+
+@Composable
+private fun UserAgent() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ var userAgent by remember { mutableStateOf(oldUserAgent) }
+ newUserAgent = userAgent
+ OutlinedTextField(
+ value = userAgent,
+ placeholder = { Text(stringResource(R.string.user_agent)) },
+ onValueChange = {
+ userAgent = it
+ newUserAgent = userAgent
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.user_agent)
+ alertMessage.value = ctx.getString(R.string.user_agent_help)
+ showAlert.value = true
+ },
+ textStyle = androidx.compose.ui.text.TextStyle(
+ fontSize = 18.sp, color = LocalCustomColors.current.itemText),
+ label = { LabelText(stringResource(R.string.user_agent)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+ )
+ }
+}
+
+@Composable
+private fun AudioSettings(navController: NavController) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ Text(
+ text = stringResource(R.string.audio_settings),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ navController.navigate("audio")
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp,
+ fontWeight = FontWeight. Bold
+ )
+ }
+}
+
+@Composable
+private fun Ringtone() {
+ val ctx = LocalContext.current
+ oldRingtoneUri = if (Preferences(ctx).ringtoneUri == "")
+ RingtoneManager.getActualDefaultRingtoneUri(ctx, RingtoneManager.TYPE_RINGTONE).toString()
+ else
+ Preferences(ctx).ringtoneUri!!
+ newRingtoneUri = oldRingtoneUri
+ val launcher = rememberLauncherForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result: ActivityResult ->
+ if (result.resultCode == RESULT_OK) {
+ val uri: Uri? = if (VERSION.SDK_INT >= 33)
+ result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Uri::class.java)
+ else
+ @Suppress("DEPRECATION")
+ result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
+ if (uri != null)
+ newRingtoneUri = uri.toString()
+ }
+ }
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ Text(
+ text = stringResource(R.string.ringtone),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER)
+ intent.putExtra(
+ RingtoneManager.EXTRA_RINGTONE_TYPE,
+ RingtoneManager.TYPE_RINGTONE
+ )
+ intent.putExtra(
+ RingtoneManager.EXTRA_RINGTONE_TITLE,
+ ctx.getString(R.string.select_ringtone)
+ )
+ intent.putExtra(
+ RingtoneManager.EXTRA_RINGTONE_EXISTING_URI,
+ newRingtoneUri.toUri()
+ )
+ intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false)
+ intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
+ launcher.launch(intent)
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold
+ )
+ }
+}
+
+@Composable
+private fun BatteryOptimizations() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ val batterySettingsLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ val powerManager = ctx.getSystemService(POWER_SERVICE) as PowerManager
+ newBatteryOptimizations = !powerManager.isIgnoringBatteryOptimizations(ctx.packageName)
+ }
+ Text(text = stringResource(R.string.battery_optimizations),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.battery_optimizations)
+ alertMessage.value = ctx.getString(R.string.battery_optimizations_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ var battery by remember { mutableStateOf(oldBatteryOptimizations) }
+ Switch(
+ checked = battery,
+ onCheckedChange = {
+ battery = it
+ newBatteryOptimizations = battery
+ batterySettingsLauncher.launch(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
+ }
+ )
+ }
+}
+@Composable
+private fun Contacts(activity: Activity) {
+ val showAlertDialog = remember { mutableStateOf(false) }
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp)
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.contacts),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.contacts)
+ alertMessage.value = ctx.getString(R.string.contacts_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ val isDropDownExpanded = remember {
+ mutableStateOf(false)
+ }
+ val contactNames = listOf("baresip", "Android", "Both")
+ val contactValues = listOf("baresip", "android", "both")
+ val itemPosition = remember {
+ mutableIntStateOf(contactValues.indexOf(oldContactsMode))
+ }
+ val requestPermissionsLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestMultiplePermissions()
+ ) {}
+ val contactsPermissions = arrayOf(Manifest.permission.READ_CONTACTS,
+ Manifest.permission.WRITE_CONTACTS)
+ Box {
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.clickable {
+ isDropDownExpanded.value = true
+ }
+ ) {
+ Text(text = contactNames[itemPosition.intValue],
+ color = LocalCustomColors.current.itemText)
+ CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
+ tint = LocalCustomColors.current.itemText)
+ }
+ DropdownMenu(
+ expanded = isDropDownExpanded.value,
+ onDismissRequest = {
+ isDropDownExpanded.value = false
+ }) {
+ contactNames.forEachIndexed { index, name ->
+ DropdownMenuItem(text = {
+ Text(text = name)
+ },
+ onClick = {
+ isDropDownExpanded.value = false
+ val mode = contactValues[index]
+ if (mode != "baresip" && !Utils.checkPermissions(ctx, contactsPermissions)) {
+ dialogTitle.value = ctx.getString(R.string.consent_request)
+ dialogMessage.value = ctx.getString(R.string.contacts_consent)
+ positiveText.value = ctx.getString(R.string.accept)
+ onPositiveClicked.value = {
+ showDialog.value = false
+ newContactsMode = mode
+ if (ContextCompat.checkSelfPermission(
+ ctx,
+ Manifest.permission.READ_CONTACTS
+ ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(
+ ctx,
+ Manifest.permission.WRITE_CONTACTS
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ Log.d(TAG, "Contacts permissions already granted")
+ } else {
+ if (shouldShowRequestPermissionRationale(
+ activity, Manifest.permission.READ_CONTACTS) ||
+ shouldShowRequestPermissionRationale(
+ activity, Manifest.permission.WRITE_CONTACTS))
+ showAlertDialog.value = true
+ else
+ requestPermissionsLauncher.launch(
+ arrayOf(
+ Manifest.permission.READ_CONTACTS,
+ Manifest.permission.WRITE_CONTACTS
+ )
+ )
+ }
+ }
+ negativeText.value = ctx.getString(R.string.deny)
+ onNegativeClicked.value = {
+ itemPosition.intValue = contactValues.indexOf(oldContactsMode)
+ negativeText.value = ""
+ }
+ showDialog.value = true
+ }
+ else {
+ itemPosition.intValue = index
+ newContactsMode = contactValues[index]
+ }
+ })
+ if (index < 2)
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = LocalCustomColors.current.itemText
+ )
+ }
+ }
+ }
+ if (showAlertDialog.value)
+ AlertDialog(
+ showDialog = showAlertDialog,
+ title = stringResource(R.string.notice),
+ message = stringResource(R.string.no_android_contacts),
+ positiveButtonText = stringResource(R.string.ok),
+ onPositiveClicked = { requestPermissionsLauncher.launch(
+ arrayOf(
+ Manifest.permission.READ_CONTACTS,
+ Manifest.permission.WRITE_CONTACTS
+ )
+ )},
+ negativeButtonText = "",
+ onNegativeClicked = {},
+ )
+ }
+}
+
+@Composable
+private fun DarkTheme() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.dark_theme),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.dark_theme)
+ alertMessage.value = ctx.getString(R.string.dark_theme_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ var darkTheme by remember { mutableStateOf(oldDarkTheme) }
+ Switch(
+ checked = darkTheme,
+ onCheckedChange = {
+ darkTheme = it
+ newDarkTheme = darkTheme
+ }
+ )
+ }
+}
+
+@RequiresApi(29)
+@Composable
+private fun DefaultDialer() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.default_phone_app),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.default_phone_app)
+ alertMessage.value = ctx.getString(R.string.default_phone_app_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ val roleManager = ctx.getSystemService(ROLE_SERVICE) as RoleManager
+ val dialerRoleRequest = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ Log.d(TAG, "dialerRoleRequest result: $result")
+ newDefaultDialer = roleManager.isRoleHeld(RoleManager.ROLE_DIALER)
+ }
+ var defaultDialer by remember { mutableStateOf(oldDefaultDialer) }
+ Switch(
+ checked = defaultDialer,
+ onCheckedChange = {
+ defaultDialer = it
+ newDefaultDialer = defaultDialer
+ if (it) {
+ if (!roleManager.isRoleAvailable(RoleManager.ROLE_DIALER)) {
+ alertTitle.value = ctx.getString(R.string.alert)
+ alertMessage.value = ctx.getString(R.string.dialer_role_not_available)
+ showAlert.value = true
+ }
+ else
+ if (!roleManager.isRoleHeld(RoleManager.ROLE_DIALER))
+ dialerRoleRequest.launch(roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER))
+ } else {
+ try {
+ dialerRoleRequest.launch(Intent("android.settings.MANAGE_DEFAULT_APPS_SETTINGS"))
+ } catch (e: ActivityNotFoundException) {
+ Log.e(TAG, "ActivityNotFound exception: ${e.message}")
+ }
+ }
+ }
+ )
+ }
+}
+
+@Composable
+private fun Debug() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.debug),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.debug)
+ alertMessage.value = ctx.getString(R.string.debug_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ var db by remember { mutableStateOf(oldDebug) }
+ Switch(
+ checked = db,
+ onCheckedChange = {
+ db = it
+ newDebug = db
+ }
+ )
+ }
+}
+
+@Composable
+private fun SipTrace() {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.sip_trace),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.sip_trace)
+ alertMessage.value = ctx.getString(R.string.sip_trace_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ var sipTrace by remember { mutableStateOf(oldSipTrace) }
+ Switch(
+ checked = sipTrace,
+ onCheckedChange = {
+ sipTrace = it
+ newSipTrace = sipTrace
+ }
+ )
+ }
+}
+
+@Composable
+private fun Reset(onRestartApp: () -> Unit) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(end = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ val ctx = LocalContext.current
+ Text(text = stringResource(R.string.reset_config),
+ modifier = Modifier
+ .weight(1f)
+ .clickable {
+ alertTitle.value = ctx.getString(R.string.reset_config)
+ alertMessage.value = ctx.getString(R.string.reset_config_help)
+ showAlert.value = true
+ },
+ color = LocalCustomColors.current.itemText,
+ fontSize = 18.sp)
+ var reset by remember { mutableStateOf(false) }
+ Switch(
+ checked = reset,
+ onCheckedChange = {
+ dialogTitle.value = ctx.getString(R.string.confirmation)
+ dialogMessage.value = ctx.getString(R.string.reset_config_alert)
+ positiveText.value = ctx.getString(R.string.reset)
+ onPositiveClicked.value = {
+ Config.reset()
+ onRestartApp()
+ }
+ negativeText.value = ctx.getString(R.string.cancel)
+ onNegativeClicked.value = {
+ reset = false
+ negativeText.value = ""
+ }
+ showDialog.value = true
+ }
+ )
+ }
+}
+
+private fun checkOnClick(ctx: Context) {
+
+ if (oldAutoStart != newAutoStart) {
+ Config.replaceVariable(
+ "auto_start",
+ if (newAutoStart) "yes" else "no"
+ )
+ save = true
+ }
+
+ val listenAddr = newListenAddr.trim()
+ if (listenAddr != oldListenAddr) {
+ if ((listenAddr != "") && !Utils.checkIpPort(listenAddr)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = "${ctx.getString(R.string.invalid_listen_address)}: $listenAddr"
+ showAlert.value = true
+ return
+ }
+ Config.replaceVariable("sip_listen", listenAddr)
+ save = true
+ restart = true
+ }
+
+ if (oldAddressFamily != newAddressFamily) {
+ Config.replaceVariable("net_af", newAddressFamily)
+ save = true
+ restart = true
+ }
+
+ var dnsServers = newDnsServers.lowercase(Locale.ROOT).replace(" ", "")
+ dnsServers = addMissingPorts(dnsServers)
+ if (dnsServers != oldDnsServers.replace(" ", "")) {
+ if (!checkDnsServers(dnsServers)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = "${ctx.getString(R.string.invalid_dns_servers)}: $dnsServers"
+ showAlert.value = true
+ return
+ }
+ Config.removeVariable("dns_server")
+ if (dnsServers.isNotEmpty()) {
+ for (server in dnsServers.split(","))
+ Config.addVariable("dns_server", server)
+ Config.replaceVariable("dyn_dns", "no")
+ if (Api.net_use_nameserver(dnsServers) != 0) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = "${ctx.getString(R.string.failed_to_set_dns_servers)}: $dnsServers"
+ showAlert.value = true
+ return
+ }
+ } else {
+ Config.replaceVariable("dyn_dns", "yes")
+ Config.updateDnsServers(BaresipService.dnsServers)
+ }
+ // Api.net_dns_debug()
+ save = true
+ }
+
+ if (oldVerifyServer != newVerifyServer) {
+ Config.replaceVariable("sip_verify_server", if (newVerifyServer) "yes" else "no")
+ Api.config_verify_server_set(newVerifyServer)
+ save = true
+ }
+
+ newUserAgent = newUserAgent.trim()
+ if (newUserAgent != oldUserAgent) {
+ if ((newUserAgent != "") && !Utils.checkServerVal(newUserAgent)) {
+ alertTitle.value = ctx.getString(R.string.notice)
+ alertMessage.value = "${ctx.getString(R.string.invalid_user_agent)}: $newUserAgent"
+ showAlert.value = true
+ return
+ }
+ if (newUserAgent != "")
+ Config.replaceVariable("user_agent", newUserAgent)
+ else
+ Config.removeVariable("user_agent")
+ save = true
+ restart = true
+ }
+
+ Log.d(TAG, "Old/new ringtone: $oldRingtoneUri / $newRingtoneUri")
+ if (newRingtoneUri != oldRingtoneUri) {
+ Preferences(ctx).ringtoneUri = newRingtoneUri
+ BaresipService.rt = RingtoneManager.getRingtone(ctx, newRingtoneUri.toUri())
+ }
+ if (oldContactsMode != newContactsMode) {
+ Config.replaceVariable("contacts_mode", newContactsMode)
+ BaresipService.contactsMode = newContactsMode
+ val baresipService = Intent(ctx, BaresipService::class.java)
+ when (newContactsMode) {
+ "baresip" -> {
+ BaresipService.androidContacts.value = listOf()
+ Contact.restoreBaresipContacts()
+ baresipService.action = "Stop Content Observer"
+ }
+ "android" -> {
+ BaresipService.baresipContacts.value = mutableListOf()
+ Contact.loadAndroidContacts(ctx)
+ baresipService.action = "Start Content Observer"
+ }
+ "both" -> {
+ Contact.restoreBaresipContacts()
+ Contact.loadAndroidContacts(ctx)
+ baresipService.action = "Start Content Observer"
+ }
+ }
+ Contact.contactsUpdate()
+ ctx.startService(baresipService)
+ save = true
+ }
+
+ val newDisplayTheme = if (newDarkTheme)
+ AppCompatDelegate.MODE_NIGHT_YES
+ else
+ AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+ if (oldDarkTheme != newDarkTheme) {
+ Preferences(ctx).displayTheme = newDisplayTheme
+ BaresipService.darkTheme.value = newDarkTheme
+ AppCompatDelegate.setDefaultNightMode(newDisplayTheme)
+ Config.replaceVariable("dark_theme",
+ if (newDarkTheme) "yes" else "no")
+ save = true
+ }
+
+ if (oldDebug != newDebug) {
+ val logLevelString = if (newDebug) "0" else "2"
+ Config.replaceVariable("log_level", logLevelString)
+ Api.log_level_set(logLevelString.toInt())
+ Log.logLevelSet(logLevelString.toInt())
+ save = true
+ }
+
+ if (oldSipTrace != newSipTrace) {
+ BaresipService.sipTrace = newSipTrace
+ Api.uag_enable_sip_trace(newSipTrace)
+ }
+
+ if (save) Config.save()
+}
+
+private fun isAppearOnTopPermissionGranted(ctx: Context): Boolean {
+ return Settings.canDrawOverlays(ctx)
+}
+
+private fun addMissingPorts(addressList: String): String {
+ if (addressList == "") return ""
+ var result = ""
+ for (addr in addressList.split(","))
+ result = if (Utils.checkIpPort(addr)) {
+ "$result,$addr"
+ } else {
+ if (Utils.checkIpV4(addr))
+ "$result,$addr:53"
+ else
+ "$result,[$addr]:53"
+ }
+ return result.substring(1)
+}
+
+private fun checkDnsServers(dnsServers: String): Boolean {
+ if (dnsServers.isEmpty()) return true
+ for (server in dnsServers.split(","))
+ if (!Utils.checkIpPort(server.trim())) return false
+ return true
+}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/TaskReceiver.kt b/app/src/main/kotlin/com/tutpro/baresip/TaskReceiver.kt
index da22c457..0d6bd2a6 100644
--- a/app/src/main/kotlin/com/tutpro/baresip/TaskReceiver.kt
+++ b/app/src/main/kotlin/com/tutpro/baresip/TaskReceiver.kt
@@ -31,13 +31,13 @@ class TaskReceiver : BroadcastReceiver() {
Api.account_set_regint(acc.accp, REGISTRATION_INTERVAL)
Api.ua_register(ua.uap)
acc.regint = Api.account_regint(acc.accp)
- AccountsActivity.saveAccounts()
+ Account.saveAccounts()
} else {
Log.d(TAG, "TaskReceiver: un-registering $aor")
Api.account_set_regint(acc.accp, 0)
Api.ua_unregister(ua.uap)
acc.regint = Api.account_regint(acc.accp)
- AccountsActivity.saveAccounts()
+ Account.saveAccounts()
}
}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt b/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt
index d4e731d5..abc7df4b 100644
--- a/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt
+++ b/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt
@@ -45,6 +45,23 @@ class UserAgent(val uap: Long) {
return null
}
+ fun reRegister() {
+ this.status = R.drawable.circle_yellow
+ if (this.account.regint == 0)
+ Api.ua_unregister(this.uap)
+ else
+ Api.ua_register(this.uap)
+ }
+
+ fun makeDefault() {
+ val index = uas.value.indexOf(this)
+ val updatedUas = uas.value.toMutableList()
+ updatedUas.removeAt(index)
+ updatedUas.add(0, this)
+ uas.value = updatedUas.toList()
+ uasStatus.value = statusMap()
+ }
+
companion object {
fun ofAor(aor: String): UserAgent? {
diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt
index 4a1c082a..def6ee91 100644
--- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt
+++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt
@@ -7,7 +7,6 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
-import android.graphics.Bitmap
import android.media.AudioAttributes
import android.media.AudioDeviceInfo
import android.media.AudioManager
@@ -62,7 +61,6 @@ import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
-import androidx.core.graphics.scale
object Utils {
@@ -231,7 +229,7 @@ object Utils {
fun checkUriUser(user: String): Boolean {
val escaped = """%(\d|A|B|C|D|E|F|a|b|c|d|e|f){2}""".toRegex()
escaped.replace(user, "").forEach {
- if (!(it.isLetterOrDigit() || "-_.!~*\'()&=+\$,;?/".contains(it))) return false }
+ if (!(it.isLetterOrDigit() || "-_.!~*\'()&=+$,;?/".contains(it))) return false }
return user.isNotEmpty() && !checkIpV4(user) && !checkIpV6(user)
}
@@ -239,7 +237,7 @@ object Utils {
val parts = domain.split(".")
for (p in parts) {
if (p.endsWith("-") || p.startsWith("-") ||
- !Regex("^[-a-zA-Z0-9]+\$").matches(p))
+ !Regex("^[-a-zA-Z0-9]+$").matches(p))
return false
}
return true
@@ -333,7 +331,7 @@ object Utils {
}
fun isTelNumber(no: String): Boolean {
- return no.isNotEmpty() && Regex("^([+][1-9])?[0-9- (),*#]{0,24}\$").matches(no)
+ return no.isNotEmpty() && Regex("^([+][1-9])?[0-9- (),*#]{0,24}$").matches(no)
}
fun isTelUri(uri: String): Boolean {
@@ -383,7 +381,7 @@ object Utils {
}
private fun checkToken(token: String): Boolean {
- return Regex("^[-a-zA-Z0-9.!%*_+`'~]+\$").matches(token)
+ return Regex("^[-a-zA-Z0-9.!%*_+`'~]+$").matches(token)
}
@Suppress("unused")
@@ -585,22 +583,6 @@ object Utils {
name
}
- fun saveBitmap(bitmap: Bitmap, file: File): Boolean {
- if (file.exists()) file.delete()
- try {
- val out = FileOutputStream(file)
- val scaledBitmap = bitmap.scale(96, 96)
- scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
- out.flush()
- out.close()
- Log.d(TAG, "Saved bitmap to ${file.absolutePath} of length ${file.length()}")
- } catch (e: Exception) {
- Log.e(TAG, "Failed to save bitmap to ${file.absolutePath}: $e")
- return false
- }
- return true
- }
-
class Crypto(val salt: ByteArray, val iter: Int, val iv: ByteArray, val data: ByteArray): Serializable {
companion object {
private const val serialVersionUID: Long = -29238082928391L
@@ -805,11 +787,6 @@ object Utils {
rnd.nextInt(256))
}
- fun addActivity(activity: String) {
- if ((BaresipService.activities.isEmpty()) || (BaresipService.activities[0] != activity))
- BaresipService.activities.add(0, activity)
- }
-
fun requestDismissKeyguard(activity: Activity) {
val kgm = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
kgm.requestDismissKeyguard(activity, null)
@@ -1018,4 +995,39 @@ object Utils {
Api.AAudio_close_stream()
}
+
+ /*fun listFilesInDirectory(directoryPath: String): List {
+ val directory = File(directoryPath)
+ if (!directory.exists()) {
+ Log.w(TAG, "Directory does not exist: $directoryPath")
+ return emptyList()
+ }
+ if (!directory.isDirectory) {
+ Log.w(TAG, "Path is not a directory: $directoryPath")
+ return emptyList()
+ }
+ val files = directory.listFiles()
+ if (files == null) {
+ Log.e(
+ TAG,
+ "Failed to list files in directory (listFiles returned null): $directoryPath"
+ )
+ return emptyList()
+ }
+ return files.filter { it.isFile }
+ }*/
+
+ /*@SuppressLint("RestrictedApi")
+ fun printBackStack(navController: NavController) {
+ Log.e(TAG, "---- Current Navigation Back Stack ----")
+ navController.currentBackStack.value.forEachIndexed { index, navBackStackEntry ->
+ val route = navBackStackEntry.destination.route
+ val arguments = navBackStackEntry.arguments?.let { bundle ->
+ bundle.keySet().joinToString(", ") { key -> "$key=${bundle.get(key)}" }
+ } ?: "null"
+ Log.e(TAG, "$index: Route='${route}', Args=[$arguments], ID=${navBackStackEntry.id}")
+ }
+ Log.e(TAG, "--------------------------------------")
+ }*/
+
}
diff --git a/app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt b/app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt
index 402cf3ff..d0aa2d72 100644
--- a/app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt
+++ b/app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt
@@ -1,10 +1,23 @@
package com.tutpro.baresip
import android.app.Application
+import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import androidx.compose.runtime.State
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
+
+sealed class NavigationCommand {
+ data class NavigateToChat(val aor: String, val peer: String?) : NavigationCommand()
+ data class NavigateToCalls(val aor: String) : NavigationCommand()
+ object NavigateToHome: NavigationCommand()
+ // Add other navigation commands as needed
+}
class ViewModel(application: Application) : AndroidViewModel(application) {
@@ -15,4 +28,96 @@ class ViewModel(application: Application) : AndroidViewModel(application) {
_selectedAor.value = newValue
}
+ private val _speakerIcon = MutableStateFlow(R.drawable.speaker_off)
+ val speakerIcon: StateFlow = _speakerIcon.asStateFlow()
+
+ fun updateSpeakerIcon(iconResId: Int) {
+ _speakerIcon.value = iconResId
+ }
+
+ private val _micIcon = MutableStateFlow(R.drawable.mic_on)
+ val micIcon: StateFlow = _micIcon.asStateFlow()
+
+ fun updateMicIcon(iconResId: Int) {
+ _micIcon.value = iconResId
+ }
+
+ private val _vmIcon = MutableStateFlow(R.drawable.voicemail)
+ val vmIcon: StateFlow = _vmIcon.asStateFlow()
+
+ fun updateVmIcon(newIcon: Int) {
+ _vmIcon.value = newIcon
+ }
+
+ private val _showVmIcon = MutableStateFlow(false)
+ val showVmIcon: StateFlow = _showVmIcon.asStateFlow()
+
+ fun updateShowVmIcon(show: Boolean) {
+ _showVmIcon.value = show
+ }
+
+ private val _messagesIcon = MutableStateFlow(R.drawable.messages)
+ val messagesIcon: StateFlow = _messagesIcon.asStateFlow()
+
+ fun updateMessagesIcon(newIcon: Int) {
+ _messagesIcon.value = newIcon
+ }
+
+ private val _callsIcon = MutableStateFlow(R.drawable.calls)
+ val callsIcon: StateFlow = _callsIcon.asStateFlow()
+
+ fun updateCallsIcon(newIcon: Int) {
+ _callsIcon.value = newIcon
+ }
+
+ private val _dialpadIcon = MutableStateFlow(R.drawable.dialpad_off)
+ val dialpadIcon: StateFlow = _dialpadIcon.asStateFlow()
+
+ fun updateDialpadIcon(newIcon: Int) {
+ _dialpadIcon.value = newIcon
+ }
+
+ private val _audioSettingsResult = mutableStateOf(null)
+ val audioSettingsResult: State get() = _audioSettingsResult
+
+ fun setAudioSettingsResult(result: Boolean) {
+ _audioSettingsResult.value = result
+ }
+
+ fun clearAudioSettingsResult() {
+ _audioSettingsResult.value = null
+ }
+
+ private val _selectedCallRow = MutableStateFlow(null)
+
+ fun selectCallRow(callRow: CallRow) {
+ _selectedCallRow.value = callRow
+ }
+
+ fun consumeSelectedCallRow(): CallRow? {
+ val callRow = _selectedCallRow.value
+ _selectedCallRow.value = null
+ return callRow
+ }
+
+ private val _navigationCommand = MutableSharedFlow()
+ val navigationCommand = _navigationCommand.asSharedFlow()
+
+ fun onNewMessageReceived(aor: String, peer: String) {
+ viewModelScope.launch {
+ _navigationCommand.emit(NavigationCommand.NavigateToChat(aor, peer))
+ }
+ }
+
+ fun navigateToCalls(aor: String) {
+ viewModelScope.launch {
+ _navigationCommand.emit(NavigationCommand.NavigateToCalls(aor))
+ }
+ }
+
+ fun navigateToHome() {
+ viewModelScope.launch {
+ _navigationCommand.emit(NavigationCommand.NavigateToHome)
+ }
+ }
}
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index a486fb6f..a8c92444 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -26,6 +26,7 @@
Voit lisätä puheluiden ja viestien kohteet yhteystietoihin pitkällä kosketuksella.
Pitkillä kosketuksilla voit myös poistaa puheluita, viestiketjuja, viestejä ja yhteystietoja.
Voit lisätä/poistaa yhteystiedon avatar-kuvan koskettamalla yhteystiedon ikonia lyhyesti/pitkästi.
+ Audio-koodekin saa pitkällä kosketuksella käytöön/pois käytöstä.
Katso lisätietoja Wiki-sivulta.
Tietosuoja
@@ -69,6 +70,7 @@
Voit lisätä puheluiden ja viestien kohteet yhteystietoihin pitkällä kosketuksella.
Pitkillä kosketuksilla voit myös poistaa puheluita, viestiketjuja, viestejä ja yhteystietoja.
Voit lisätä/poistaa yhteystiedon avatar-kuvan koskettamalla yhteystiedon ikonia lyhyesti/pitkästi.
+ Audio- ja video-koodekin saa pitkällä kosketuksella käytöön/pois käytöstä.
Katso lisätietoja Wiki-sivulta.
Tunnetut ongelmat
@@ -255,6 +257,8 @@
Vastaa
Hylkää
+ Vastaa
+ Talleta
Puhelu soittajalta
Vastaamaton puhelu soittajalta
Vastaamattomia puheluita
@@ -593,8 +597,13 @@
Et voi käyttää Androidin yhteystietoja ilman Yhteystiedot-lupaa.
Audiofokus on evätty!
Tarvittavat luvat
- baresip tarvitsee Mikrofoni-luvan puheluita varten ja Lähellä olevat laitteet -luvan Bluetooth-mikrofonin/kaiuttimen havaitsemista varten ja Ilmoitukset-luvan ilmoitusten lähettämistä varten.
- baresip+ tarvitsee Mikrofoni-luvan puheluita varten, Kamera-luvan videopuheluita varten ja Lähellä olevat laitteet -luvan Bluetooth-mikrofonin/kaiuttimen havaitsemista varten ja Ilmoitukset-luvan ilmoitusten näyttämistä varten.
+ baresip tarvitsee Mikrofoni-luvan puheluita varten, Lähellä olevat laitteet -luvan
+ Bluetooth-mikrofonin/kaiuttimen havaitsemista varten, Ilmoitukset-luvan ilmoitusten lähettämistä varten
+ ja Android versiossa 9 Tallennustila-luvan Talleta/Palauta-toimintoja varten.
+ baresip+ tarvitsee Mikrofoni-luvan puheluita varten,
+ Kamera-luvan videopuheluita varten, Lähellä olevat laitteet -luvan Bluetooth-mikrofonin/kaiuttimen
+ havaitsemista varten, Ilmoitukset-luvan ilmoitusten näyttämistä varten ja Android versiossa 9
+ Tallennustila-luvan Talleta/Palauta-toimintoja varten.
Puheluiden talletus
Jos aktivoitu, uudet soitetut ja vastatut puhelut talletetaan.
Tallennukset voi kuunnella Puhelutiedot-sivulla.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 630da876..d7e9ed2d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -29,6 +29,7 @@
Peers of calls and messages can be added to contacts by long touches.
Long touches can also be used to remove calls, chats, messages, and contacts.
Touch/long touch on contact icon can be used to install/remove image avatar.
+ Long touch on an audio codec can be used to enable/disable the codec.
See Wiki for more information.
Privacy Policy
@@ -72,6 +73,7 @@
Peers of calls and messages can be added to contacts by long touches.
Long touches can also be used to remove calls, chats, messages, and contacts.
Touch/long touch of contact icon can be used to install/remove image avatar.
+ Long touch on an audio or video codec can be used to enable/disable the codec.
See Wiki for more
information.
@@ -240,6 +242,8 @@
Answer
Reject
+ Reply
+ Save
Incoming call from
Missed call from
Missed calls
@@ -575,11 +579,13 @@
Audio focus denied!
Permissions rationale
baresip needs \"Microphone\" permission for voice calls,
- \"Nearby devices\" permission for Bluetooth microphone/speaker detection, and
- \"Notifications\" permission for posting notifications.
- baresip+ needs \"Microphone\" permission for voice calls,
- \"Camera\" permission for video calls, \"Nearby devices\" permission for Bluetooth
- microphone/speaker detection, and \"Notifications\" permission for posting notifications.
+ \"Nearby devices\" permission for Bluetooth microphone/speaker detection,
+ \"Notifications\" permission for posting notifications, and in Android 9
+ \"Storage\" permission for Backup/Restore operations.
+ baresip+ needs \"Microphone\" permission
+ for voice calls, \"Camera\" permission for video calls, \"Nearby devices\" permission
+ for Bluetooth microphone/speaker detection, \"Notifications\" permission for posting
+ notifications, and in Android 9 \"Storage\" permissions for Backup/Restore operations.
Call Recording
If activated, new incoming and outgoing calls will be recorded.
Recordings can be played on Call Details page
@@ -587,4 +593,4 @@
If activated during call, microphone is muted.
Speakerphone
If activated, audio is played via device speakerphone.
-
\ No newline at end of file
+
diff --git a/libbaresip-android b/libbaresip-android
index d244a177..75d9f03a 160000
--- a/libbaresip-android
+++ b/libbaresip-android
@@ -1 +1 @@
-Subproject commit d244a177e9a1cd344064f45f450de4110376d1b2
+Subproject commit 75d9f03a02f35c29a38f529b4bf9b594f94e2e06